Я уже много лет являюсь поклонником серии "Effective XXX", начатой Скотом Мейерсом в 1997-м году с его “Effective C++”. Книги из этой серии содержат несколько десятков советов о вашем любимом языке программирования, рассказыавя о том, что делать стоит, а чего – нет. Такие книги легко читать, и они являются отличным источником для размышлений.
И хотя эти книги являются раем для читателя, их невероятно сложно писать. Чтобы понять это, достаточно попробовать написать статейку из серии "Используйте/не используйте эту возможность языка C#" или просто вспомните какой-нибудь холи-ворчик у себя в коллективе, который начался невинной фразой, "а давайте везде будем использовать as вместо оператора приведения типов" или другой подобной фразы.
Проблема любых советов из серии используй/не используй/избегай в том обилии исключений, которые следуют за любым подобным правилом. Вот, например, стоит ли использовать изменяемые значимые типы (mutable value types)? Любой программист, пришедший в .NET из С++ ответит положительно (ведь так быстрее!), потом он прочитает о проблемах и его мнение наверняка изменится на противоположное. После чего, в его голове может возникнуть барьер, который он уже не сможет преодолеть даже тогда, когда ему нужно будет пожертвовать безопасностью в угоду эффективности и использовать структуры в своем коде.
Все это может привести к "заякоренности" мышления и карго-культу, в результате которого целая команда может избегать тех или иных возможностей языка программирования, только из-за того, что кто-то там сказал о том, что это плохо.
Именно поэтому при чтении (или слушании) любых советов из серии, делать что-то или нет, нужно постараться понять причину этого совета. Это позволит обобщить этот совет и использовать его в более широком контексте, и, что не менее важно, это даст вам понимание того, когда нужно следовать этому совету, а когда пришло время его нарушить!
Несложно догадаться, что я бы не писал обо всем этом, если бы к "Effective C#" у меня не было бы вопросов. К моему сожалению, этих вопросов оказалось значительно больше, чем я рассчитывал.
"Мелкие" неточности
Одной из ключевых особенностей книг и статей Джона Скита является его строгость в использовании терминов и понятий, а также точность в описании языковых конструкций. Джон может пожертвовать глубиной, но при этом будет хотя бы сноска о том, что существует ряд граничных условий. Автор "Effective C#" в этом плане не столь строг и последователен, в результате чего появляются ляпы разной величины.
Неточность #1. Переопределение статических методов
"You never override the static Object.Reference and static Object.Equals() because they provide the correct tests, regardless of the runtime type."
Автор несколько раз пишет о том, что не нужно переопределять (override или redefine) статические методы, поскольку они ведут себя положенным образом. Проблема лишь в том, что сделать этого в языке C# не можем в любом случае.
Любая статья или книга является своего рода "абстракцией", которая акцентирует внимание на ключевых характеристиках, опуская при этом ненужные подробности. Сложность при этом заключается в том, чтобы это "абстрагирование" не затрагивало важные аспекты.
Неточность #2. Время жизни локальных переменных
All reference types, even local variables, are allocated on the heap. Every local variable of a reference type becomes garbage as soon as the function exits.
Сборщик мусора сложнее, чем может показаться и локальные переменные могут быть достижимыми для сборщика еще до завершения метода!
Неточность #3. Об операторе ==
"No matter what type is involved, a == a is always true."
На самом деле, для платформы .NET это правило исполняется не всегда. А вы можете привести пример, когда это условие нарушается?
Неточность #4. Виртуальность интерфейсов
"Members declared in interfaces are not virtual – at least, not by default.
...
Interface methods are not virtual. When you implement an interface, you are declaring a concrete implementation of a particular contract in that type."
Я согласен с тем, что наследование интерфейсов имеет свои особенности. Да, любая реализация метода интерфейса неявно будет "закрытой" (sealed), но ведь это же характеристика метода, реализующего метод интерфейса, а не самого интерфейса.
Неточность #5. Порядок создания объектов
"Here is the order of operations for constructing the first instance of a type:
- Static variable storage is set to 0.
- Static variable initializers execute.
- Static constructors for the base class execute.
- The static constructor executes.
- …"
Теперь-то мы знаем, что порядок вызова статических конструкторов не так-то прост. На самом деле, вызов статического конструктора наследника не приводит к вызову статического конструктора базового класса, а при создании экземпляра наследника статический конструктор даже создаваемого типа может не вызываться.
Но если предыдущие неточности можно отнести к моему буквоедству, то есть ряд и более серьезных моментов.
Неточность #6. Об итераторах коллекций
В совете 21 автор приводит замечательный пример использования закрытых внутренних классов для реализации итераторов коллекций. В качестве подтверждения этого подхода приводится такая цитата:
"The .NET Framework designers followed the same pattern with the other collection classes: Dictionary<T> contains a private DictionaryEnumerator<T>, Queue<T> contains a QueueEnumerator<T>, and so on. The enumerator class being private gives many advantages…"
Здесь две проблемы: во-первых, енумераторы коллекций являются структурами, а во-вторых, эти структуры являются открытыми. Итераторы являются известных источником непонятного поведения, поскольку итераторы по своей природе являются изменяемыми, а изменяемые структуры – дело очень опасное.
Я бы понял, если бы этот пример был гипотетическим (ведь для своих собственных коллекций этот пример не так и плох). Но в книге с названием "Effective C#" такие вольности и ошибки мне кажутся весьма странными.
Неточность #7. Об Equals и GetHashCode
К методам Equals и GetHashCode масса вопросов (Неточность #3 тоже из этой области, кстати, ответ мой на вопрос такой: это Double.NaN).
Со всеми этими Equals и GetHashCode и правда ногу можно сломать, но, во-первых, автор этой теме посвятил не один раздел, а во-вторых, мы именно для того и читаем продвинутые книги, чтобы найти ответы на подобные вопросы.
Так, например, автор пишет следующее по поводу необходимости переопределения метода GetHashCode:
"For reference types, it works but is inefficient. For value types, the base class version is often incorrect."
Неэффективность метода Object.GetHashCode с точки зрения автора объясняется следующим. В качестве значения GetHashCode для объектов используется внутренний счетчик, который увеличивается при создании каждого объекта. Поскольку значение счетчика не является случайным, то полученный в результате хеш-код будет приводить к частым коллизиям и низкой эффективности поиска.
Простые эксперименты показывают, что CLR ведет себя несколько иначе (вызов (new object()).GetHashCode() дважды приводит к получению совсем разных значений). А быстрое гугление доказывает, что реализация несколько сложнее.
Вторая часть высказывания еще более странная. На самом деле, даже реализация метода GetHashCode возвращающая 42 является корректной. Да, неэффективной, но абсолютно корректной. В этот раз автор настаивает на том, что реализация метода ValueType.GetHashCode просто возвращает хэш-код первого поля. Это почти так, и реализация по умолчанию может возвращать трансформированный хэш-код первого поля. Да, эта реализация может приводить к бОльшему количеству коллизий, но назвать ее некорректной никак нельзя.
Да и вообще, если мы влезаем в подобные детали реализации, то лучше расписывать их более подробно, объясняя при этом не только текущее поведение, но еще и причины такой реализации.
Неужели настолько все плохо?
Будет ли книга полезна сильно зависит от вашего опыта и, что самое главное, отношения к читаемому материалу. Если опыта достаточно много, то вы просто не найдете ничего нового. Если же вы относитесь к чужим советам со здоровым прагматизмом и не читали Скита или де Смета, то эта книга может быть хорошим источником для обсуждения разных возможностей языка C#.
ПРИМЕЧАНИЕ
Если все так неоднозначно, то что эта книга делает списке классических книг по C#/.NET? Я честно признаюсь, что мое мнение было основано на первом издании этой книги, прочитанной где-то 4-5 лет назад. Добавил бы я ее, если бы составлял этот список сегодня? Не уверен! Тем не менее, это достаточно уникальная книга в своем роде, пусть и с неидеальным исполнением. Поэтому до появления реальной замены на рынке я ее из этого списка удалять не буду.
Оценка: 3
UPDATE
Вот, кстати, рецензия Скита: "Book Review: Effective C# (2nd edition) by Bill Wagner”, в котором Скит делится похожими мыслями (возможно более тактично, поскольку они с Биллом лично знакомы).
double.NaN != double.NaN
ОтветитьУдалитьАга, это баян, но почему-то автор и рецензенты это дело провтыкали.
ОтветитьУдалитьПричем я же уверен, что у книги должны быть опытные тех. редакторы. В общем, странно видеть такие ляпы от соавтора аннотаций к "C# Reference Manual".
Фраза "локальные переменные становятся достижимыми еще до их завершения!" ... короче, суть я понял, но только потому, что знал и раньше про это)))
ОтветитьУдалить@Andrew: спасибо, перефразировал мысль.
ОтветитьУдалитьВ классе можно переопределить ==.
ОтветитьУдалить@Евгений: речь идет не о том, можно перегрузить оператор ==, а о том, есть ли существующие стандартные типы, для которых это свойство не выполняется.
ОтветитьУдалитьЕсть еще например System.Data.SqlTypes.SqlInt32.Null :)
ОтветитьУдалить@Евгений: ну там и сигнатура оператора == другая:) он возвращает null, что согласуется с логикой реляционных баз данных.
ОтветитьУдалитьА вот чтобы operator == вертал именно false, других примеров я не знаю.