null – это не false. Часть 3

Возвращаемся к теме нашего обсуждения: мы бы хотели позволить пользователям «перегружать» операторы & и | в языке C#, и если мы хотим перегружать эти операторы, то кажется, что должна существовать возможность перегрузки операторов && и ||.

Но тогда мы сталкиваемся с большой проблемой. Обычно мы перегружаем оператор путем создания метода:

class C
{
string s;
public C(string s) { this.s = s; }
public override string ToString() { return s; }
public static C operator +(C x, C y) { return new C(x.s + "+" + y.s); }
}
...
Console.WriteLine(new C("123") + new C("456")); // "123+456"

Но аргументы метода вычисляются «жадно» (eagerly). Поэтому мы не можем написать:

public static C operator &&(C x, C y) {... что угодно... }

Поскольку в этом случае, когда вы напишите:

C c = GetFirstC() && GetSecondC();

Код будет преобразован компилятором в:

C c = C.op_ShortCircuitAnd(GetFirstC(), GetSecondC());

Что, очевидно, приведет к вычислению обоих операндов не зависимо от того, будет ли левый операнд равен «true» или «false».

Конечно же, с современном C# в нашем распоряжении есть тип, использование которого говорит нам: «произвести вычисления в будущем, по требованию»; это тип Func<T>. Мы можем реализовать данную версию так:

public static C operator &&(C x, Func<C> fy) {... что угодно... }

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

C c = C.op_ShortCircuitAnd(GetFirstC(), ()=>GetSecondC());

Этот вариант будет прекрасно работать, хотя и содержит ряд проблем. Во-первых, он потенциально менее эффективный; даже если мы никогда не будем использовать второй аргумент, мы все равно тратим ресурсы на создание делегата. Во-вторых, C# 1.0 не содержал ни лямбда-выражений, ни обобщенных типов делегатов, поэтому во времена разработки первой версии языка этот вариант бы не подошел.

При решении этой задачи мы приняли, что на самом деле, здесь имеют место две вещи. Во-первых, мы должны решить, нужно ли вычислять правый операнд или нет. Если нам не нужно вычислять правый операнд, то результат может равняться левому операнду. Если мы вычисляем правый операнд, то нам нужно объединить результаты вычисления обоих операндов с помощью оператора без оптимизации вычисления логических выражений (non-short-circuiting operator).

Первая операция (т.е. определение того, нужно ли вычислять правый операнд) требует операторов operator true и operator false. Эти операторы дают ответ на вопрос: «требует ли этот операнд вычисления второго или нет?».

Но теперь мы сталкиваемся с другой проблемой. В прошлый раз у нас была проблема с операторами && и || при использовании значений, отличных от true или false. А именно, означает ли выражение x && y «вычислить y тогда и только тогда, когда x равен true» или же «вычислить y тогда и только тогда, когда x не равен false»? Очевидно, что для булевых выражений оба эти варианты эквивалентны, но они не эквивалентны для nullable Boolean. И они также не эквивалентны для определенных пользователем операторов. В конце концов, если у вас есть непротиворечивый способ преобразования вашего типа к true или false, вы просто реализовали бы неявное преобразование вашего типа к bool и использовали бы встроенные операторы && и ||.

Команда проектировщиков языка C# 1.0 решили, что правило должно быть таким: «вычислять y тогда и только тогда, когда x не false». Мы реализовали это правило с помощью оператора «operator false», который возвращает true, когда x нужно рассматривать как false, и false, когда x нельзя рассматривать как false. Понимаю, что звучит запутанно. Возможно, пример сможет помочь:

class C
{
string s;
public C(string s) { this.s = s; }
public override string ToString() { return s; }
public static C operator &(C x, C y) { return new C(x.s + "&" + y.s); }
public static C operator |(C x, C y) { return new C(x.s + "|" + y.s); }
public static bool operator true(C x) { return x.s == "true"; }
public static bool operator false(C x) { return x.s == "false"; }
}
...
C ctrue = new C("true");
C cfalse = new C("false");
C cfrob = new C("frob");

Console.WriteLine(ctrue && cfrob); // true&frob
Console.WriteLine(cfalse && cfrob); // false
Console.WriteLine(cfrob && cfrob); // frob&frob

В данном случае x && y реализовано так:

C temp = x;
C result = C.op_false(temp)? temp : temp & y;

Аналогично, выражение x || y использует «operator true» подобным же образом.

Малоизвестным фактом является то, что если для пользовательского типа вы можете использовать && и ||, то вы можете использовать его в инструкциях, принимающих bool, таких как if и while. Это означает, что вы можете написать следующее:

if (ctrue && cfrob)...

Поскольку этот код эквивалентен:

C temp = ctrue;
C result = C.op_false(temp)? temp : temp & cfrb;
bool b = C.op_true(result);
if (b)...

Правда здорово?

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