Comments 12
Вообще такие вещи обычно делают через самописный Fody-weaver на пару десятков строк, меняющий место вызова базового конструктора в сгенерированном MSIL.
Мы с коллегой недавно с таким же энтузиазмом, как в этой статье обсуждали интернирование строк, и так и не придумали, где с пользой использовать эти знания.
Сдаётся мне, что такое может понадобится в случае наследования от third-party класса. Например, если нужно внедрить свой hook до инициализации его состояния
Бывает нужно как минимум при необходимости преобразовать аргументы конструктора, передать дальше И сохранить у себя. Пока такой переданный дальше аргумент один и не зависим от остального — можно нагородить статический метод с приватным конструктором. Когда начинаются кросс-зависимости — становится грустно.
Второй сценарий — когда кто-то догадался в базовом классе из конструктора дергать виртуальный метод, а нам теперь позарез надо поведение этого метода менять в зависимости от того, что нам прилетело в конструктор.
Вот обычная ситуация. Пусть есть класс X и у него три наследника: A, B и C. И у этих наследников в конструкторе требуется довольно сложная инициализация, практически идентичная, но отличающаяся в некоторых нюансах. Естественно, возникает желание написать код инициализации один раз в конструкторе класса X, а нюансы реализовать за счёт вызова виртуального метода, который будет переопределён в A, B и C в соответствии с их спецификой. В C# это, в принципе, работает (в отличие от C++, в котором таблица виртуальных методов инициализируется по мере вызовов унаследованных конструкторов, поэтому в момент выполнения конструктора X вызов виртуального метода приведёт к вызову метода, определённого в X, а не перекрытого наследником). Но если нужно, чтобы перекрытые методы как-то зависели от полей, объявленных в классах-наследниках, а значения этих полей — от параметров, переданных в конструкторы, то в C# не существует нормального решения этого вопроса. Присваивание этим полям значений невозможно выполнить до вызова унаследованного конструктора, а без этого вызываемые им виртуальные методы работают некорректно. Уже несколько раз с такой задачей сталкивался, и ни разу не удалось найти нормального решения.
Вариант, который предложил автор статьи, завязан на рефлексию и, соответственно, имеет целый ряд недостатков вроде потери производительности и невозможности обнаружения ошибок во время компиляции. Но это вина не автора, а разработчиков языка, которые не предусмотрели такую очень нужную возможность.
Я бы тоже хотел увидеть пример ситуации, когда это реально нужно.
в 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, в которой так делать было нельзя (по крайней мере, тогда; как сейчас — уже не знаю). Пришлось довольно нефигово приседать, чтобы пофиксить все связанные с такой миграцией проблемы (да, само собой как-то нечаянно получилось, что код оказался завязан на это поведение; самому неприятно).
Во всяком случае, точно могу сказать, что проблема не надуманная, а в рабочем (допустим, не вполне аккуратно написанном) коде сама собою образовалась.
Вызываем конструктор базового типа в произвольном месте