Pull to refresh

Comments 12

Вообще такие вещи обычно делают через самописный Fody-weaver на пару десятков строк, меняющий место вызова базового конструктора в сгенерированном MSIL.

А можно пример реальной ситуации, когда это нужно?

Мы с коллегой недавно с таким же энтузиазмом, как в этой статье обсуждали интернирование строк, и так и не придумали, где с пользой использовать эти знания.

Сдаётся мне, что такое может понадобится в случае наследования от third-party класса. Например, если нужно внедрить свой hook до инициализации его состояния

Бывает нужно как минимум при необходимости преобразовать аргументы конструктора, передать дальше И сохранить у себя. Пока такой переданный дальше аргумент один и не зависим от остального — можно нагородить статический метод с приватным конструктором. Когда начинаются кросс-зависимости — становится грустно.
Второй сценарий — когда кто-то догадался в базовом классе из конструктора дергать виртуальный метод, а нам теперь позарез надо поведение этого метода менять в зависимости от того, что нам прилетело в конструктор.

Я регулярно сталкиваюсь с ситуациями, когда это нужно. Тем более что раньше я много писал на Delphi, где унаследованный конструктор можно вызвать в любом месте без проблем, и это было очень удобно.

Вот обычная ситуация. Пусть есть класс X и у него три наследника: A, B и C. И у этих наследников в конструкторе требуется довольно сложная инициализация, практически идентичная, но отличающаяся в некоторых нюансах. Естественно, возникает желание написать код инициализации один раз в конструкторе класса X, а нюансы реализовать за счёт вызова виртуального метода, который будет переопределён в A, B и C в соответствии с их спецификой. В C# это, в принципе, работает (в отличие от C++, в котором таблица виртуальных методов инициализируется по мере вызовов унаследованных конструкторов, поэтому в момент выполнения конструктора X вызов виртуального метода приведёт к вызову метода, определённого в X, а не перекрытого наследником). Но если нужно, чтобы перекрытые методы как-то зависели от полей, объявленных в классах-наследниках, а значения этих полей — от параметров, переданных в конструкторы, то в C# не существует нормального решения этого вопроса. Присваивание этим полям значений невозможно выполнить до вызова унаследованного конструктора, а без этого вызываемые им виртуальные методы работают некорректно. Уже несколько раз с такой задачей сталкивался, и ни разу не удалось найти нормального решения.

Вариант, который предложил автор статьи, завязан на рефлексию и, соответственно, имеет целый ряд недостатков вроде потери производительности и невозможности обнаружения ошибок во время компиляции. Но это вина не автора, а разработчиков языка, которые не предусмотрели такую очень нужную возможность.

Я бы тоже хотел увидеть пример ситуации, когда это реально нужно.

А вот я бы никогда не хотел сталкиваться с ситуацией когда это кому-то понадобилось. В том проекте, который мы сейчас делаем америкосы в C++ ной библиотеке переопределили new и delete, и один наш разработчик уже третий день с матами это из их кода выковыривает, потому что условия поменялись и грязный хак больше не работает. Тут точно та же ситуация.
в Java конструктор базового типа можно вызвать в любом месте конструктора производного типа


Это вряд ли. Конструктор базового класса всегда вызывается до выполнения конструктора производного. Даже сам оператор вызова конструктора базового типа «фейковый»: он лишь определяет с какими аргументами он будет вызываться, он отделен от остального кода конструктора просто пропастью:

class B extends A {
    int a = 123;
    int b;
    B(int c) {
        // тут под класс B только выделена память, а сам он еще не инициализирован.
        // Его нестатические поля нельзя использовать внутри A(...) !!!
        super(c); // Это не вызов A(), а всего лишь указание, с какими параметрами вызвать A(). A() будет вызван в любом случае ДО выполнения B()
        // тут пропасть, в которой инициализируется класс B,
        // так что дальше его нестатические поля уже можно использовать
        b = a + c;
    }
}


Вообще давно уже не «проявляю эрудицию» в таких случаях — если не глубоко знаешь область, то чаще выглядишь дураком
Конструктор базового класса всегда вызывается до выполнения конструктора производного.

Не совсем, всё-таки конструктор производного вызывает конструктор базового.
Для проверки возьмём код:


class Base {
  public Base() {
    System.out.println("Base()");
  }
}

class Child extends Base {
  private Child(Object o) {
    super();
  }

  public Child() {
    this(doSomething());
    System.out.println("Child()");
  }

  private static Object doSomething() {
    System.out.println("do something");
    return null;
  }
}

public class Main {
  public static void main(String[] args) {
    new Child();
  }
}

Запустим:


$ java Main
do something
Base()
Child()

Видим, что мы смогли выполнить статический метод до вызова конструктора базового класса.
Посмотрим скомпилированный класс:


$ javap -p -c Child
Compiled from "Main.java"
class Child extends Base {
  private Child(java.lang.Object);
    Code:
       0: aload_0
       1: invokespecial #1    // Method Base."<init>":()V
       4: return

  public Child();
    Code:
       0: aload_0
       1: invokestatic  #2    // Method doSomething:()Ljava/lang/Object;
       4: invokespecial #3    // Method "<init>":(Ljava/lang/Object;)V
       7: getstatic     #4    // Field java/lang/System.out:Ljava/io/PrintStream;
    <...>
}

Видим, что статический метод вызывается раньше конструктора (invokestatic перед invokespecial), но при этом он находится внутри конструктора дочернего класса.

Ах, эти бесполезные на практике, но такие «показательные» вопросы на собеседованиях!

То ли дело C++, берешь вызываешь себе placement new где хочешь

Конструктор родительского класса можно вызывать в любом месте конструктора дочернего класса в JS и CoffeeScript.


Однажды я на это очень хорошо напоролся, когда переводил кодовую базу с CoffeeScript на одну из относительно ранних версий TypeScript, в которой так делать было нельзя (по крайней мере, тогда; как сейчас — уже не знаю). Пришлось довольно нефигово приседать, чтобы пофиксить все связанные с такой миграцией проблемы (да, само собой как-то нечаянно получилось, что код оказался завязан на это поведение; самому неприятно).


Во всяком случае, точно могу сказать, что проблема не надуманная, а в рабочем (допустим, не вполне аккуратно написанном) коде сама собою образовалась.

Sign up to leave a comment.

Articles