Из плюсов: довольно приличное количество мыслей для размышлений, разбитых по разделам. Начинаешь читать главы по Equals/GetHashCode, любые несогласия с автором вынуждают к определенному ресерчу, который позволяет понять, где же на самом деле правда. И таких «вбросов» достаточно много; на часть из них сам автор дает вменяемые ответы, а вот ответы на другие вбросы придется искать в другом месте.
Возможно недостатки подпортили впечатление сильнее, в результате чего положительные моменты не так сильно отпечатались в памяти. Все будет зависеть от того, что вы читали до этого. Если для вас в книге Скита не много нового, то эту книгу нет смысла читать. Если же у вас пара лет опыта работы с платформой .NET и языком C#, то в книге Вагнера будет много полезного.
Речь немного не об этом. Речь о выделенном, что при создании экземпляра вначале вызывается статический конструктор базового класса, а потом статический конструктор наследника. В некоторых случаях (при наличии атрибута beforefieldinit у типа), при создании экземпляра наследника даже вызов статического конструктора наследника не гарантируется (подробности здесь).
Но даже в базовом случае, порядок вызова статических конструкторов противоположен порядку вызова экземплярных конструкторов: экземплярные конструкторы взываются от базового к наследнику, а статические конструкторы — от наследника к базовому.
Вот пруф:
internal class Base
{
public Base()
{
Console.WriteLine("Base.ctor");
}
static Base()
{
Console.WriteLine("Base.cctor");
}
}
internal class Derived : Base
{
public Derived()
{
Console.WriteLine("Derived.ctor");
}
static Derived()
{
Console.WriteLine("Derived.cctor");
}
}
При вызове new Derived() на экране мы получим следующее:
Спасибо за добавление контекста, в этом случае неточность становится более четкой:) Дело в том, что в .NET-е, рефлексивность обязан обеспечивать метод Equals, но ее нет у оператора ==.
Это одна из причин (помимо того, что оператор == не полиморфный), почему для определения эквивалентности объектов в том же Dictionary используется именно Equals, а не оператор ==.
И Double.NaN является известным примером, когда Equals остается рефлексивным, а оператор == — нет.
Я же не пишу о возврате в С++, но многие мои коллеги, пришедшие из С++ изменяют свое отношение к структурам в .NET-е, после понимания количества проблем, которые могут с ними произойти.
Кстати, на на конструктор копирования структуры я даже не знаю, как с хаками повлиять. Разве что через CLR Profiling API, но даже в этом я не очень уверен.
Я так и не понял, какая связь между конструктором по умолчанию и процессом копирования?
Конструктор по умолчанию устанавливает исходный вариант создаваемому объекту, конструктор копирования делает побитовую (как в случае структур) копию объекта, что автоматически сохраняет его инвариант.
При копировании не должен вызываться конструктор по умолчанию, этого не происходит в том же С++, и этого не происходит в .NET при бинарной или DataContract десериализации объекта. Это нормальное и ожидаемое поведение.
Еще раз: к каким логическим проблемам приведет наличие явного конструктора без параметров в контексте передачи структур по значению и почему его можно создать в IL-е?
В С++, например, есть четкое разделение между конструктором компирования и конструктором по умолчанию и никаких логических проблем при этом нет.
Здесь есть такое же разделение, но при этом конструктор копирования «генерируется» не компилятором, а CLR. Так что я не вижу никакой связи между отсутствием конструкторов по умолчанию в языке C# и передачей по значению с автоматическим копированием всех полей структуры.
Более того, this в конструкторе структуры по сути является out параметром, если «дефолтный» конструктор не вызывается, и является ref-параметром, если «дефолтный» конструктор вызывается:
struct SomeStruct
{
private int _i;
private double _d;
public SomeStruct(int i)
: this()
{
// this в теле метода является ref SomeStruct
CreateOrUpdate(ref this);
}
public SomeStruct(double d)
{
// this() не вызывается, значит this является out SomeStruct:
Create(out this);
}
public static void CreateOrUpdate(ref SomeStruct str)
{
str = new SomeStruct();
}
public static void Create(out SomeStruct str)
{
str = new SomeStruct();
}
}
Во-первых, вот небольшая линка, подтверждающая мое мнение о сходстве между методом Dispose в .NET стеке и деструктором.
Во-вторых, в С++ сам объект сможет разрушить это состояние лишь в том случае, если его будут правильно использовать. Создайте объект в куче и уже никто и ничего разрушать не будет. Так что деструктор и метод Dispose оба будут работать правильно только в том случае, если вызывающий код работает правильно. В этом вопросе их поведение аналогично, просто в первом случае (в С++) вам нужна локальная переменная, а во-втором случае (в C#) — нужно обрамление объекта в блок using.
Ну, и в третьих, а как же С++/CLI? Есть ли там RAII или нет? Ведь там вместо деструктора вызывается метод Dispose?
Тут, кажись, все сложнее.
Если следовать некоторому здравому смыслу, то в некоторых случаях (например, при работе с енуменраторами коллекций из BCL) боксинга быть не должно, поскольку это бы убило весь смысл от использования структуры в качестве енумератора.
С другой стороны, если подходить к этому делу формально, то все становится немного сложнее. Компиллер языка C# преобразует using для не-nullable значимых типов в следующую конструкцию:
А такой код (с вызовом метода Dispose через приведение к интерфейсу приводит к генерации IL-инструкции constraint, которая ведет себя очень хитро: упаковки не будет, если структура реализует метод интерфейса обычным образом и будет, если метод интерфейса реализован явно
У меня конструктор locker-а захватывает ресурс (блокировку) и освобождает его в методе Dispose, который вызывается автоматически при выходе из области видимости. Что семантически аналогично автоматическому вызову деструктора и используется, нужно сказать, для этих же целей (формально, вызов метода Dispose разрушает инвариант объекта и дальнейшее использование такого объекта приводит к неопределенному поведению). В С++ такое случается реже, но тоже не исключено, если мы, вдруг, где-то сохраним ссылку на объект, деструктор которого будет вызван.
Если следовать вашей логике, то на C++/CLI RAII тоже невозможен, а ведь там на уровне языка зашит вызов метод Dispose, вместо вызова финализатора, совсем аналогично тому, как действует конструкция using.
Ну так try-with-resources и блок using — это как раз и есть представление идиомы RAII в Java (начиная с 7-й версии) и в C#, соответственно. Без этих же конструкций, идиома RAII делается «вручную» с помощью блока finally, просто в этом случае мы делаем все руками.
З.Ы. В .NET гарантии вызова финализатора точно такие же, как и в Java, т.е. никаких.
Достаточно погулять по Джерси или НЮ, чтобы понять какова доступность iPhone-а для жителей штатов. Там айфон — это не удел распальцованных ребят, это обычная железка для большинства населения. Совершенно нормальная ситуация, когда у камрада бомжеватого (с нашей точки зрения вида) будет кобура с айфоном.
Возможно недостатки подпортили впечатление сильнее, в результате чего положительные моменты не так сильно отпечатались в памяти. Все будет зависеть от того, что вы читали до этого. Если для вас в книге Скита не много нового, то эту книгу нет смысла читать. Если же у вас пара лет опыта работы с платформой .NET и языком C#, то в книге Вагнера будет много полезного.
double.NaN.Equals(double.NaN) — true
double.NaN == double.NaN — false
Просто если бы не работал Equals, то вы бы не смогли из словаря double.NaN достать.
Но даже в базовом случае, порядок вызова статических конструкторов противоположен порядку вызова экземплярных конструкторов: экземплярные конструкторы взываются от базового к наследнику, а статические конструкторы — от наследника к базовому.
Вот пруф:
При вызове new Derived() на экране мы получим следующее:
Derived.cctor Base.cctor Base.ctor Derived.ctor
Речь же идет о том, есть ли тип, желательно в BCL, для которого рефлексивность оператора == не выполняется.
Это одна из причин (помимо того, что оператор == не полиморфный), почему для определения эквивалентности объектов в том же Dictionary используется именно Equals, а не оператор ==.
И Double.NaN является известным примером, когда Equals остается рефлексивным, а оператор == — нет.
Конструктор по умолчанию устанавливает исходный вариант создаваемому объекту, конструктор копирования делает побитовую (как в случае структур) копию объекта, что автоматически сохраняет его инвариант.
При копировании не должен вызываться конструктор по умолчанию, этого не происходит в том же С++, и этого не происходит в .NET при бинарной или DataContract десериализации объекта. Это нормальное и ожидаемое поведение.
Еще раз: к каким логическим проблемам приведет наличие явного конструктора без параметров в контексте передачи структур по значению и почему его можно создать в IL-е?
Здесь есть такое же разделение, но при этом конструктор копирования «генерируется» не компилятором, а CLR. Так что я не вижу никакой связи между отсутствием конструкторов по умолчанию в языке C# и передачей по значению с автоматическим копированием всех полей структуры.
Во-вторых, в С++ сам объект сможет разрушить это состояние лишь в том случае, если его будут правильно использовать. Создайте объект в куче и уже никто и ничего разрушать не будет. Так что деструктор и метод Dispose оба будут работать правильно только в том случае, если вызывающий код работает правильно. В этом вопросе их поведение аналогично, просто в первом случае (в С++) вам нужна локальная переменная, а во-втором случае (в C#) — нужно обрамление объекта в блок using.
Ну, и в третьих, а как же С++/CLI? Есть ли там RAII или нет? Ведь там вместо деструктора вызывается метод Dispose?
Если следовать некоторому здравому смыслу, то в некоторых случаях (например, при работе с енуменраторами коллекций из BCL) боксинга быть не должно, поскольку это бы убило весь смысл от использования структуры в качестве енумератора.
С другой стороны, если подходить к этому делу формально, то все становится немного сложнее. Компиллер языка C# преобразует using для не-nullable значимых типов в следующую конструкцию:
А такой код (с вызовом метода Dispose через приведение к интерфейсу приводит к генерации IL-инструкции constraint, которая ведет себя очень хитро: упаковки не будет, если структура реализует метод интерфейса обычным образом и будет, если метод интерфейса реализован явно
Если следовать вашей логике, то на C++/CLI RAII тоже невозможен, а ведь там на уровне языка зашит вызов метод Dispose, вместо вызова финализатора, совсем аналогично тому, как действует конструкция using.
З.Ы. В .NET гарантии вызова финализатора точно такие же, как и в Java, т.е. никаких.
Финализатор — это не блок finally, это «деструктор» вызываемый сборщиком мусора.