четверг, 25 июля 2013 г.

Оператор уточнения типа или static cast в C#

Нередко в моих программах на C# возникает необходимость уточнить тип переменной, то есть попросить компилятор посчитать, что тип переменной не тот, что объявлен, а один из базовых.

Рассмотрим на примере. Допустим, имеется группа методов:

void M(IList<int> x) { } // 1
void M(IReadOnlyList<int> x) { } // 2
void M(IList<string> x) { } // 3
void M(IReadOnlyList<string> x) { } // 4

И нам…

void X(Collection<int> x) {
  // …необходимо вызвать второй из них, принимающий: IReadOnlyList<int>.
}

Что мы обычно делаем? Применяем оператор приведения типа:

void X(Collection<int> x) {
  M((IReadOnlyList<int>)x);
}

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

void X(Collection<int> x) {
  M((IReadOnlyList<string>)x);
}

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

Можно обойтись и без приведения типа:

void X(Collection<int> x) {
  IReadOnlyList<int> y = x;
  M(y);
}

Это несколько многословно, но мне кажется самым лучшим решением - максимально понятно и эффективно.

В более синтаксически продвинутых языках, например в Nemerle, для подобных случаев существует специальный оператор статического приведения типа, синтаксис которого отличается от обычного приведения - вот это было бы очень полезно иметь и в C# (а заодно уж, и отдельный оператор для боксинга-анбоксинга не помешал бы, ну да чего уж :о). Вот как применение этого оператора могло бы выглядеть:

void X(Collection<int> x) {
  M(x : IReadOnlyList<int>);
}

Кажется, тут очевидно, что имеется в виду именно уточнение (двоеточие) - считать в данном случае, что переменная x имеет тип IReadOnlyList<int>.

Так вот, как [внезапно] вдруг оказалось, такой оператор в C# уже давно есть! Посмотрите:

void X(Collection<int> x) {
  M(x ?? default(IReadOnlyList<int>));
}

И всё. Достаточно использовать null-coalescing operator. Единственное, чем использование этого оператора в данном случае грозит - более сложным IL. Но и тут имеется перспектива - должно быть не сложно добавить в компилятор оптимизацию: в случае, когда про выражение в правой части оператора (то, что после ??) можно на этапе компиляции сказать, что оно всегда null, а это можно совершенно определённо сказать о выражении default здесь:

A default-value-expression is a constant expression (§7.19) if the type is a reference type or a type parameter that is known to be a reference type (§10.1.5).

- никакой специльной логики генерировать не нужно - правая часть на значение результата не повлияет.

Теперь попробую использовать для уточнения типа null-coalescing operator, посмотрим, через какое-то время, к чему это приведёт.

2 комментария:

  1. Оператор default имеет совсем другую семантику, так что такой код соберет при чтении много пожеланий его автору :)

    ОтветитьУдалить
  2. Это, в первую очередь, и смущает - с непривычки, кажется, ищешь додвох, а его нет.

    Идея с дефолтом пришла только вчера и сегодня вчерашний код неприязни не вызвал :о) Интересно посмотреть, что будет через полгодика :о)

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