Share via


Плюсы и минусы неявной типизации

Одной из наиболее сомнительных возможностей, когда либо добавленных в язык, является объявление неявно типизированных локальных переменных, a.k.a «var». Даже сегодня, несколько лет спустя, я все еще нахожу статьи, в которых рассматриваются плюсы и минусы этой возможности. Меня часто поделиться своим мнением об этом, ну что ж, вот оно.

Прежде всего, давайте определимся с назначением кода. Для этой статьи давайте считать, что целью кода является решение бизнес-задач.

Конечно же, это не является целью любого кода. Ассемблер, который я писал во время прохождения своего курса CS 242 многие годы назад, я писал не для решения какой-либо бизнес-задачи; скорее, я делал это для того, чтобы понять, как работает ассемблер; этот код преследовал педагогические цели. Цель кода, который я пишу для решения задач на Project Euler, также не решает никаких бизнес-задач; я это делаю ради собственного удовольствия. Я уверен, что есть люди, которые пишут код исключительно ради эстетического удовольствия, как вид искусства. Существует множество причин писать код, но в этой статье я сделаю разумное предположение, что люди, желающие узнать, стоит использовать «var» или нет, являются профессиональными программистами, решающие сложные бизнес-задачи, будучи членами больших команд.

Обратите внимание, что под «бизнес-задачами» я не подразумеваю только бухгалтерские задачи; если вы занимаетесь анализом генома человека по гранту Национального научного фонда, то распознавание строк в последовательности является решением бизнес-задачи. И т.д. Здесь я не налагаю никаких ограничений на типы программ, решающих бизнес-задачи или на модели бизнеса.

Во-вторых, давайте определимся, о каких решениях идет речь. Здесь речь идет о том, не лучше ли использовать объявление локальной переменной следующим образом:

TheType theVariable = theInitializer;

Или

var theVariable = theInitializer;

где «TheType» является типом времени компиляции выражения theInitializer. Таким образом, я задаюсь вопросом, использовать ли «var» в сценариях, где это не вносит семантических изменений в код. Я подчеркиваю, что меня не интересует вопрос, является ли код вида:

IFoo myFoo = new FooStruct();

Лучше или хуже, чем:

var myFoo = new FooStruct();

поскольку эти два выражения делают разные вещи, так что сравнивать их нечестно. Аналогично я не хочу обсуждать странные и маловероятные крайние случаи, типа «а что если в текущей области видимости есть структура с именем var?» и т.п.

В то же время, я хочу обсудить доводы за и против тех случаев, когда есть выбор. Если вы решили использовать анонимные типы, то выбор, использовать неявно типизированные локальные переменные или нет, уже сделан:

var query = from c in customers select new {c.Name, c.Age};

Вопрос использования нормальных и анонимных типов является темой отдельной дискуссии; если вы решили, что использование анонимных типов того стоит, тогда вам просто придется использовать «var», поскольку альтернативных вариантов просто не существует.

Предполагая, что главной целью является решение бизнес-задач, давайте ответим на вопрос, а что же является хорошим кодом? Очевидно, это невероятно огромная тема, но следующие три фактора сразу приходят в голову. Хороший код:

  • работает корректно, согласно спецификации и действительно решает указанную задачу
  • понятен читателю, которому нужно понять смысл происходящего
  • позволяет с относительно низкими затратами решать новые задачи, которые появляются в процессе изменения бизнес-окружения.

Анализируя возможность использования «var» мы можем не обращать внимания на первый пункт; меня интересуют за и против, только в тех случаях, когда использование «var» не изменяет значение программы, а меняет только ее текстовое представление. Если мы изменим текст без изменения семантики, тогда по определению мы не изменим корректность кода. Аналогично, использование или неиспользование «var» не меняет другие наблюдаемые характеристики программы, такие как производительность. Вопрос использования или неиспользования «var» влияет только на читателя кода и на людей, занимающихся его сопровождением, и не влияет на скомпилированные артефакты.

Тогда какой эффект оказывает эта возможность на читателя кода?

Любой код, конечно же, является некоторой абстракцией; в этом вся причина, почему мы используем высокоуровневые языки программирования, а не берем в руки вольтметры и не программируем на уровне железа. Абстракции в коде подчеркивают некоторые аспекты решения, оставляя другие аспекты на втором плане. Хорошая абстракция прячет несущественные детали, и делает заметными – существенные. Вы вероятно знаете, что на процессорах семейства x86 при выполнении кода на языке C# возвращаемое значение обычно помещается в регистр EAX, а указатель «this» – в регистр ECX, однако вам не нужно знать об этом, чтобы писать программы на языке C#; вся эта информация полностью абстрагирована от вас.

Конечно же, далеко не всегда большее количество информации в коде означает лучший код. Посмотрите на запрос, о котором я упоминал ранее. Какой вариант проще для понимания: предложенный вариант или вариант, в котором используется обыкновенный тип без выражения запроса:

IEnumerable<NameAndAge> query = Enumerable.Select<Customers, NameAndAge>(customers, NameAndAgeExtractor);

вместе с реализацией класса NameAndAge, а также с методом NameAndAgeExtractor? Явно, вариант с запросом более абстрактный и прячет множество ненужной избыточной информации, подчеркивая при этом важные детали – мы создаем запрос, который получает имя и возраст из таблицы заказчиков. Запрос подчеркивает бизнес-задачу кода; второй же вариант подчеркивает механизм, используемый для реализации этой задачи.

Тогда вопрос, заключающийся в том, делает ли использование «var» код лучше или хуже для читателя приводит к двум дополнительным вопросам:

1) является ли акцент на типе переменной важным для понимания кода? и,

2) если да, является ли указание типа при объявлении необходимым для обеспечения этого акцента?

Давайте вначале рассмотрим первый вопрос. В каких случаях при чтении кода необходимо четко понимать тип переменной? Только в том случае, когда механизм работы кода – т.е. «как он работает» – более важен для читателя, нежели его семантика – т.е. «для чего он нужен».

В высокоуровневых языках, используемых для решения бизнес-задач, я предпочитаю абстрагироваться от механизма реализации, и акцентировать внимание на логике предметной области. Конечно, это не всегда так; иногда вам действительно важно, что некоторая переменная относится к типу uint, она просто должна быть типом uint, мы пользуемся ей именно потому, что это uint, и если мы заменим ее на ulong, short или что угодно другое, тогда реализация перестанет работать.

Предположим, вы пишите следующий код:

var distributionLists = MyEmailStore.Contacts(ContactKind.DistributionList);

Предположим, что неявным типом является DataTable. Важно ли читателю кода знать, что это DataTable ? Это главный вопрос. Возможно да. Возможно, корректность и понятность остальной части метода полностью зависит от понимания читателем, что distributionList – это DataTable, а не List<Contact>, IQueryable<Contact> или что-то еще.

Но будем надеяться, что это не так. Будем надеяться, что остальную часть метода можно понять, понимая, что переменная distributionLists представляет собой коллекцию списков рассылки, полученную из хранилища, в котором хранятся контакты электронной почты.

Теперь, всем тем, кто говорит, что, конечно же, знать конкретный тип всегда важно, поскольку это важно для читателя кода, я задам следующий вопрос. Давайте рассмотрим следующий код:

decimal rate = 0.0525m;
decimal principal = 200000.00m;
decimal annualFees = 100.00m;
decimal closingCosts = 1000.00m;
decimal firstPayment = principal * (rate / 12) + annualFees / 12 + closingCosts;

Давайте предположим, что вы верите в важность явного указания всех типов, поскольку это приводит к более понятному коду. Тогда почему неважно явно указывать типы для всех подвыражений? В последнем выражении присутствует как минимум четыре подвыражения, тип которых явно не указан. Если читателю важно знать, что тип переменной ‘ rate ’ – decimal , тогда почему ему неважно знать, что тип выражения ( rate / 12) также относится к типу decimal , а не, скажем, к типу int или double ?

Простой факт заключается в том, что компилятор выполняет огромный объем работы за вас по анализу типов, для выражений, типы которых никогда в явном виде не указаны в исходном коде, поскольку в большинстве случаев это будет отвлекающим шумом, нежели полезной информацией. Иногда тип объявляемой переменной также является отвлекающим шумом.

Теперь давайте рассмотрим второй вопрос. Предположим, в качестве аргумента, читателю нужно понимать тип хранилища. Нужно ли тогда его указывать? Обычно нет:

var prices = new Dictionary<string, List<decimal>>();

Читателю, возможно, полезно знать, что цены представляют собой ассоциативный массив строк к списку десятичных цифр, но это не значит, что вы должны писать следующим образом:

Dictionary<string, List<decimal>> prices = new Dictionary<string, List<decimal>>();

Использование «var» явно не ухудшает понимание этого факта.

До сих пор речь шла только лишь о чтении кода. А как насчет сопровождения? Опять-таки, использование var иногда может ухудшить сопровождаемость кода, а иногда – улучшить. Я много раз писал код подобный следующему:

var attributes = ParseAttributeList();
foreach(var attribute in attributes)
{
    if (attribute.ShortName == "Obsolete") ...

Теперь предположим, что в процессе развития этого кода я изменил тип возвращаемого значения метода ParseAttributeList на ReadOnlyCollection<AttyributeSyntax> с List<AttributeSyntax>. При использовании «var» мне вообще не придется ничего менять; код, работающий ранее, будет работать и после изменений. Использование неявно типизированных переменных позволяет выполнять простой рефакторинг, не меняющий семантики кода, с минимумом усилий. (А если рефакторинг изменяет семантику, тогда придется переписать этот код, независимо от того, используется var или нет.)

Иногда критика по поводу использования неявно типизированных локальных переменных сводится к сценариям, в которых понимание и поддержка кода, как будто бы усложняется:

var square = new Shape();
var round = new Hole();
... Через сотню строк кода ...
bool b = CanIPutThisPegInThisHole(square, round); // Работает!

Который затем изменяется на:

var square = new BandLeader("Lawrence Welk");
var round = new Ammunition();
... Через сотню строк кода ...
bool b = CanIPutThisPegInThisHole(square, round); // Не работает!

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

Вкратце, мои советы следующие:

  • Используйте var, когда без этого не обойтись; например, при использовании анонимных типов.
  • Используйте var, когда тип объявляемой переменной очевиден из инициализатора, особенно если это создание объекта. Это устраняет дублирование информации.
  • Подумайте об использовании var, когда код подчеркивает «семантику бизнес-задачи» переменной и отодвигает на второй план «механизмы» реализации.
  • Указывайте типы явно, когда это необходимо для корректного понимания кода и его сопровождения.
  • Используйте описательные имена переменных независимо от того, используете вы «var» или нет. Имена переменной должны представлять семантику переменной, а не детали реализации; имя переменной «decimalRate» является плохим; а «interestRate» – хорошим.

Оригинал статьи