четверг, 25 августа 2011 г.

Неявно типизированные поля в C#

Сегодня на кывте был задан очередной весьма интересный вопрос о том, почему в языке C# существуют неявно типизированные локальные переменные (implicitely-typed local variables) a.k.a. var, но нет неявно типизированных полей?

На самом деле, такое положение дел вовсе не случайно; так что давайте рассмотрим несколько причин, почему компилятор ведет себя именно так, а не иначе.

Во-первых, возможность использовать var, для объявления неявно типизированных локальных переменных, никогда не была самостоятельной возможностью. При разработке языка C# никто не ставил перед собой цели создать полностью неявно типизированный язык программирования, типа F#; неявная типизация была лишь одной составляющей (пусть и немаловажной) более общей концепции, известной сегодня под аббревиатурой LINQ.

Поскольку при разрабоке LINQ-а было принято решение, что завязывать разработчика только на существующие типы является слишком «злым» ограничением, то при ее реализации была предоставлена разработчику возможность возвращать последовательности анонимных классов. А раз так, то без использования неявно типизированных переменных это было бы не возможно:

IEnumerable<Customer> customers = null;
// Упс, а тип переменной result какой? IEnumerable<??>?
var result = from c in customers
                where c.Age > 33
                select new { c.Name, c.Age };

Однако использование ключевого слова var в объявлении типа никак бы не помогло решить более глобальную цель (это я опять про LINQ).

Во-вторых, даже если не залазить слишком глубоко в то, насколько сложно было бы реализовать такую возможность компиляторописателям (*), даже навскидку с ее реализацией и использованием связаны некоторые недостатки. Первый недостаток связан с тем, что неявно типизированные поля будут плохо дружить с анонимными типами.

Давайте представим себе следующий класс:

public class Foo
{
    public var someField = new {Name = "name", Value = 12};

}

Поскольку анонимные типы в .Net реализованы сейчас в виде internal-типов, то «экспортировать» этот тип за пределы текущей сборки просто напросто нельзя. Можно, конечно же, ограничить использование неявно типизированных полей только для интернал классов или приватных полей, или вообще запретить использование неявно типизированных полей с анонимными классами, но это сделает поведение двух семантически схожих конструкций языка C# слишком разным. Одна из причин появление var в C# 3.0 – это использование анонимных типов (с LINQ-ом или без), а тут получается, что неявнотипизированные поля с ними работать не будут.

Еще одним важным ограничением является то, что неявно типизированные поля вводят слишком тесную связь между инициализируемым полем и «инициализатором». Давайте рассмотрим такой пример: предположим, у нас есть класс A, который содержит “var”-поле с именем foo, которое инициализируется путем вызова статического метода Foo класса B:

public class A
{
    public static var foo = B.Foo();
}

public class B
{
    public static int Foo()
    {
        return default(int);
    }
}

Это приводит к целому ряду дополнительных вопросов: а что будет, если классы A и B расположены в разных сборках? А что если без перекомпиляции сборки A будет перекомпилирована сборка с классом B и тип возвращаемого значения метода Foo изменится с типа int на string? Или же у нас может быть еще неявно типизированное поле C.foo (т.е. поле foo в классе С), завязанное на тип поля A.foo, поле D.foo, завязанное на поле C.foo и т.д. и тогда изменение типа возвращаемого значения одной функции приведет к изменению типов полей в десятке разных классов (**). Да и вообще, поля являются более важной частью дизайна класса и его реализации по сравнению с локальными переменными, поэтому изменение «на лету» типа этого поля, только из-за того кто-то поменял сигнатуру функции в третьем модуле является не лучшей идеей.

Конечно же, можно было бы ограничить возможность использования неявно типизированных полей лишь в ограниченном наборе случаев, и запрещать использование этой возможности с анонимными типами и классами из других сборок, а оставить, например, только использование методов текущего класса. Но даже в этом случае реализация этой возможности требует слишком больших усилий на реализацию (пруф у Эрика, я тут не причемJ), что делает ее довольно низкоприоритетной в бесконечном списке улучшений, которые есть в головах разработчиков компилятора языка C#.

----------------------

(*) Залазить «в дебри» нет никакого смысла, поскольку это уже сделал Эрик Липперт в своей заметке Why no var on fields?, в которой он как раз и рассказывает о том, что реализация неявно типизированных полей потребовала бы значительно более существенных затрат на реализацию, нежели реализация неявно типизированных локальных переменных.

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

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

  1. А что будет в таком случае:
    class A {
    public int GetData() { return 1; }
    }

    class B {
    public void DoSomething() {
    var obj = new A();
    var result = obj.GetData()
    }
    }

    что если классы в разных сборках, и впоследстии автор сборки А поменяет тип возвращаемого функцией значения?

    Заранее спасибо за ответ.

    ОтветитьУдалить
  2. @valker: Да, быть беде. Мы получим ошибку во время выполнения. Но разница здесь в том, что не будет каскада ошибок. Мы поломали только лишь одну функцию и все. В случае использования неявной типизации с полями, мы можем поломать каскадно: класса A поломал B, тот поломал C, тот D и т.д.

    Кроме того, очень большая разница в границах (scope) поломки. В случае с переменными - этот скоуп ограничен лишь одной функцией, в случае полей - всеми функциями и, потенциально, всеми клиентами этого класса.

    ОтветитьУдалить
  3. Ладно с var - это не так уж критично (хотя со своими объектами анонимных классов, может быть и необходимо). А вот чего, действительно, не хватает - так это возможность вывода типов вроде:
    IVector
    {
      type Scalar;
      Scalar Length { get; };
      Scalar Component(int component);
    }
    TVector.Scalar VecMult<TVector>(TVector a, TVector b)
      where TVector : IVector

    Нет нужно всё пихать в Generic'и (выглядит больше как инжектинг):
    IVector<TScalar>
    {
      TScalar Length { get; };
      TScalar Component(int component);
    }

    TScalar VecMult<TScalar,TVector>(TVector a, TVector b)
      where TVector : IVector<TScalar>

    И всё потому - что Generic-и это просто какая-то надстройка над интерфейсами.

    ОтветитьУдалить
  4. На работе код-стандарт был - var только в тестах и там, где есть явная инициализация:
    var foo = new TypeFoo(),
    а вот так -
    var foo = getFromFunction() - уже нельзя.
    Поэтому даже если бы поля и были неявно типизированные - вряд ли кто-то кроме гиков стал их употреблять (ИМХО конечно).

    ОтветитьУдалить
  5. Думаю, если бы он был до конца честным, то написал бы просто: "НАМ ЛЕНЬ заниматься такой хренью, поэтому отвяньте". Естественно, такая гнилая отмазка негоже для ЯКОБЫ профессионалов разработки компиляторов.
    Всё, что я прочитал - фуфло полнейшее, МОЖНО и НУЖНО делать неявную типизацию полей, тем более, что КАК ПРАВИЛО ничего выводить не нужно - инициализатор даёт исчерпывающую инфу о типе.

    ОтветитьУдалить
  6. @vt2012: тролинг детектед.

    А можно меньше эмоций и больше по делу?

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