О книге Билла Вагнера «Effective C#»

в 18:02, , рубрики: .net, book review, метки: ,

О книге Билла Вагнера «Effective C#»

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

  1. Static variable storage is set to 0.
  2. Static variable initializers execute.
  3. Static constructors for the base class execute.
  4. The static constructor executes.
  5. …»

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

Но если предыдущие неточности можно отнести к моему буквоедству, то есть ряд и более серьезных моментов.

Неточность #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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js