Я уже много лет являюсь поклонником серии «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
Автор: SergeyT