понедельник, 29 июля 2013 г.

О книге Билла Вагнера “Effective C#”

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

UPDATE
Вот, кстати, рецензия Скита: "Book Review: Effective C# (2nd edition) by Bill Wagner”, в котором Скит делится похожими мыслями (возможно более тактично, поскольку они с Биллом лично знакомы).

8 комментариев:

  1. Ага, это баян, но почему-то автор и рецензенты это дело провтыкали.

    Причем я же уверен, что у книги должны быть опытные тех. редакторы. В общем, странно видеть такие ляпы от соавтора аннотаций к "C# Reference Manual".

    ОтветитьУдалить
  2. Фраза "локальные переменные становятся достижимыми еще до их завершения!" ... короче, суть я понял, но только потому, что знал и раньше про это)))

    ОтветитьУдалить
  3. @Andrew: спасибо, перефразировал мысль.

    ОтветитьУдалить
  4. В классе можно переопределить ==.

    ОтветитьУдалить
  5. @Евгений: речь идет не о том, можно перегрузить оператор ==, а о том, есть ли существующие стандартные типы, для которых это свойство не выполняется.

    ОтветитьУдалить
  6. Есть еще например System.Data.SqlTypes.SqlInt32.Null :)

    ОтветитьУдалить
  7. @Евгений: ну там и сигнатура оператора == другая:) он возвращает null, что согласуется с логикой реляционных баз данных.

    А вот чтобы operator == вертал именно false, других примеров я не знаю.

    ОтветитьУдалить