Friday, May 15, 2015

Dictionary initializer, nameof, interpolation strings and null propagation operator in C# 6

Здравствуйте, уважаемые читатели. Сегодня мы снова погрузимся в удивительный мир C# 6.0 и рассмотрим такие фишки, как использование nameof оператора, dictionary initialization, а также null propagation operator. Буду писать в своем любимом стиле, когда часть кода на языке C# сравниваю с тем кодом, который генерируется в IL. Делаю это для того чтобы помочь разработчикам, которые работают с IL-кодом и любят копаться в том, как работает та или иная фишка языка, понять, что же все-таки происходит на самом деле.
nameof
Начнем, пожалуй, с оператора nameof. Его предназначение и использование покажу на примере. Думаю, вам неоднократно приходилось видеть код, подобный коду внизу, когда вам нужно было записать некую магическую в строку в ArgumentNullException или другой тип ошибки.
public static void Print(Point point)
{
    if (point == null)
        throw new ArgumentException("point");
    Console.WriteLine("coordinate X = {0}, coordinate Y = {1}", point.X, point.Y);
}

public class Point
{
    public int X { get; set; }
    public int Y { get; set; }
}
Думаю, в вашем проекте хватает такого кода, в котором вы используете подобные “магические строки”. В C# 6 появилась возможность достать имя типа с использованием оператора nameof. Сейчас это выглядит так:
public static void Print(Point point)
{
    if (point == null)
        throw new ArgumentException(nameof(point));
    WriteLine("coordinate X = {0}, coordinate Y = {1}", point.X, point.Y);
}
Заметьте, у вас пропала магическая строка “point”, а вы достаете имя типа с переданного аргумента. Если вы посмотрите на метод Print в IL-коде, то увидите, что у вас просто копируется имя переменной.
.method public hidebysig static
    void Print (
        class NewSharpFeatures.Point point
    ) cil managed
{
    // Method begins at RVA 0x20f0
    // Code size 59 (0x3b)
    .maxstack 3
    .locals init (
        [0] bool
    )

    IL_0000: nop
    IL_0001: ldarg.0
    IL_0002: ldnull
    IL_0003: ceq
    IL_0005: stloc.0
    IL_0006: ldloc.0
    IL_0007: brfalse.s IL_0014

    IL_0009: ldstr "point"
    IL_000e: newobj instance void [mscorlib]System.ArgumentException::.ctor(string)
    IL_0013: throw

    IL_0014: ldstr "coordinate X = {0}, coordinate Y = {1}"
    IL_0019: ldarg.0
    IL_001a: callvirt instance int32 NewSharpFeatures.Point::get_X()
    IL_001f: box [mscorlib]System.Int32
    IL_0024: ldarg.0
    IL_0025: callvirt instance int32 NewSharpFeatures.Point::get_Y()
    IL_002a: box [mscorlib]System.Int32
    IL_002f: call string [mscorlib]System.String::Format(stringobjectobject)
    IL_0034: call void [mscorlib]System.Console::WriteLine(string)
    IL_0039: nop
    IL_003a: ret
// end of method Program::Print
Здесь в строке 0009 мы сохраняем ссылку на наш объект-строку в стек. Ничего сверхъестественного.
Вот еще один пример использования оператора nameof:
public static void NameofTest()
{
    var message = "test";
    var name = nameof(message);
    WriteLine(name);

    WriteLine(nameof(Point));

    int? n = null;
    WriteLine(nameof(n));
}
Здесь на уровне IL-кода происходит то же самое, что и в предыдущем примере.
.method public hidebysig static
    void NameofTest () cil managed
{
    // Method begins at RVA 0x20b0
    // Code size 51 (0x33)
    .maxstack 1
    .locals init (
        [0] string,
        [1] string,
        [2] valuetype [mscorlib]System.Nullable`1<int32>
    )

    IL_0000: nop
    IL_0001: ldstr "test"
    IL_0006: stloc.0
    IL_0007: ldstr "message"
    IL_000c: stloc.1
    IL_000d: ldloc.1
    IL_000e: call void [mscorlib]System.Console::WriteLine(string)
    IL_0013: nop
    IL_0014: ldstr "Point"
    IL_0019: call void [mscorlib]System.Console::WriteLine(string)
    IL_001e: nop
    IL_001f: ldloca.s 2
    IL_0021: initobj valuetype [mscorlib]System.Nullable`1<int32>
    IL_0027: ldstr "n"
    IL_002c: call void [mscorlib]System.Console::WriteLine(string)
    IL_0031: nop
    IL_0032: ret
// end of method Program::NameofTest
Лично я считаю, что добавление этого оператора как минимум улучшит красоту кода и избавит нас от “магических строк”, когда нужно пробросить какое-то исключение. В других случаях применение этого оператора будет не так популярно, как в случае обработки ошибок. 
String Interpolation
Также в новый C# добавилось немного синтаксического сахара, который позволяет скрывать функцию string.Format в более компактный вид с помощью записи $””. Как это выглядит, можно посмотреть в примере ниже.
var point = new Point { X = 3, Y = 5 };
var message = $"coordinate X = {point.X}, coordinate Y = {point.Y}";
WriteLine(message);
Для C# 5 и ниже эту запись можно переписать в следующую:
var point = new Point { X = 3, Y = 5 };
var message = string.Format("coordinate X = {0}, coordinate Y = {1}", point.X, point.Y);
Console.WriteLine(message);
На уровне IL-кода она так и интерпретируется.
.method private hidebysig static
    void Main (
        string[] args
    ) cil managed
{
    // Method begins at RVA 0x2050
    // Code size 84 (0x54)
    .maxstack 3
    .entrypoint
    .locals init (
        [0] class NewSharpFeatures.Point,
        [1] string,
        [2] class NewSharpFeatures.Point
    )

    IL_0000: nop
    IL_0001: newobj instance void NewSharpFeatures.Point::.ctor()
    IL_0006: stloc.2
    IL_0007: ldloc.2
    IL_0008: ldc.i4.3
    IL_0009: callvirt instance void NewSharpFeatures.Point::set_X(int32)
    IL_000e: nop
    IL_000f: ldloc.2
    IL_0010: ldc.i4.5
    IL_0011: callvirt instance void NewSharpFeatures.Point::set_Y(int32)
    IL_0016: nop
    IL_0017: ldloc.2
    IL_0018: stloc.0
    IL_0019: ldstr "coordinate X = {0}, coordinate Y = {1}"
    IL_001e: ldloc.0
    IL_001f: callvirt instance int32 NewSharpFeatures.Point::get_X()
    IL_0024: box [mscorlib]System.Int32
    IL_0029: ldloc.0
    IL_002a: callvirt instance int32 NewSharpFeatures.Point::get_Y()
    IL_002f: box [mscorlib]System.Int32
    IL_0034: call string [mscorlib]System.String::Format(stringobjectobject)
    IL_0039: stloc.1
    IL_003a: ldloc.1
    IL_003b: call void [mscorlib]System.Console::WriteLine(string)
    IL_004c: nop
    IL_004d: call string [mscorlib]System.Console::ReadLine()
    IL_0052: pop
    IL_0053: ret
// end of method Program::Main
Например, выше пример, в котором мы использовали оператор nameof в функции Print, можно переписать следующим образом:
public static void Print(Point point)
{
    if (point == null)
        throw new ArgumentException(nameof(point));
    WriteLine($"coordinate X = {point.X}, coordinate Y = {point.Y}");
}
Интерполяция строк отлично работает для простых строк, когда нужно два простых аргумента вывести через string.Format. А что если у нас решение намного сложнее. Например, попробуйте приведенный ниже пример переписать через интерполяцию строк вместо string.Format.
public static void DisgustingStringInterpolation(DateTime? beginDate, DateTime? endDate)
{
    var message = string.Format("Start date {0}, End date {1}",
        beginDate.HasValue ? beginDate.ToString() : "indefinite",
        endDate.HasValue ? endDate.ToString() : "indefinite");

    WriteLine(message);
}
Если вы сможете запихнуть это в string interpolation, а потом другие смогут без труда читать это, то вы действительно мастер. Мое субъективное мнение по этому поводу заключается в том, что это чушь собачья. Во-первых, мы не можем нормально весь код перенести на код с интерполяцией строк, а во-вторых, использование двух разных подходов с явным и неявным string.Format говорит о непродуманности использования данного подхода. Такой себе синтаксический сахар, который можно использовать только в простых случаях.
Dictionary initializer
Новый инициализатор словаря, как минимум, позволяет лучше читать понимать написанный код, так как запись становится более читабельной и короткой.
Пример для C# 5:
public Dictionary<int, string> _persons = new Dictionary<int, string>
{
    {1, "Aleksand"},
    {2, "John"},
    {3, "Petr" }
};
Пример для C# 6:
public Dictionary<int, string> _persons = new Dictionary<int, string>
{
    [1] = "Aleksand",
    [2] = "John",
    [3] = "Petr"
};
Согласитесь, для C# 6 пример выглядит намного лучше для понимания и чтения, чем для старой версии. Но и здесь есть ложка дегтя. Как написал Джон Скит, добавляя такой синтаксис, ребята из Майкрософт допустили ошибку. Например, приведенный ниже код с синтаксисом C# 5 версии и ниже выдаст ошибку, если указать случайно один и тот же ключ. Для 6-ой версии он работает на ура.
private static void DictionaryInitilize()
{
    var a = new Dictionary<string, string>
    {
        { "a", "1" },
        { "a", "2" }
    };

    WriteLine($"Keys : {a.Keys.Count}, Values : {a.Values.Count}");

    var b = new Dictionary<string, string>
    {
        ["a"] = "1" ,
        ["a"] = "2"
    };

    WriteLine($"Keys : {b.Keys.Count}, Values : {b.Values.Count}");
}
К сожалению, я не считаю такое поведение правильным. Вопрос сразу для знатоков: какое значение будет в ключе [“a”] для словаря b?
По сути, используя такой синтаксис, компилятор будет просто глотать ваши ошибки, если вы дважды случайно добавите один и тот же ключ. А так как от ошибок никто не застрахован, то мы получаем себе головную боль, чтобы проверять, а все ли мы верно заполнили, если раньше могли узнать об этом от компилятора. Думаю, вам все-таки интересно, почему в новом C# происходит глотание ошибок. Для этого нам нужно копнуть внутренности IL-кода, который приоткроет для нас занавес тайны.
.method private hidebysig static
    void DictionaryInitilize () cil managed
{
    // Method begins at RVA 0x2194
    // Code size 182 (0xb6)
    .maxstack 3
    .locals init (
        [0] class [mscorlib]System.Collections.Generic.Dictionary`2<stringstring>,
        [1] class [mscorlib]System.Collections.Generic.Dictionary`2<stringstring>,
        [2] class [mscorlib]System.Collections.Generic.Dictionary`2<stringstring>
    )

    IL_0000: nop
    IL_0001: newobj instance void class [mscorlib]System.Collections.Generic.Dictionary`2<stringstring>::.ctor()
    IL_0006: stloc.2
    IL_0007: ldloc.2
    IL_0008: ldstr "a"
    IL_000d: ldstr "1"
    IL_0012: callvirt instance void class [mscorlib]System.Collections.Generic.Dictionary`2<stringstring>::Add(!0, !1)
    IL_0017: nop
    IL_0018: ldloc.2
    IL_0019: ldstr "a"
    IL_001e: ldstr "2"
    IL_0023: callvirt instance void class [mscorlib]System.Collections.Generic.Dictionary`2<stringstring>::Add(!0, !1)
    IL_0028: nop
    IL_0029: ldloc.2
    IL_002a: stloc.0
    IL_002b: ldstr "Keys : {0}, Values : {1}"
    IL_0030: ldloc.0
    IL_0031: callvirt instance class [mscorlib]System.Collections.Generic.Dictionary`2/KeyCollection<!0, !1> class [mscorlib]System.Collections.Generic.Dictionary`2<stringstring>::get_Keys()
    IL_0036: callvirt instance int32 class [mscorlib]System.Collections.Generic.Dictionary`2/KeyCollection<stringstring>::get_Count()
    IL_003b: box [mscorlib]System.Int32
    IL_0040: ldloc.0
    IL_0041: callvirt instance class [mscorlib]System.Collections.Generic.Dictionary`2/ValueCollection<!0, !1> class [mscorlib]System.Collections.Generic.Dictionary`2<stringstring>::get_Values()
    IL_0046: callvirt instance int32 class [mscorlib]System.Collections.Generic.Dictionary`2/ValueCollection<stringstring>::get_Count()
    IL_004b: box [mscorlib]System.Int32
    IL_0050: call string [mscorlib]System.String::Format(stringobjectobject)
    IL_0055: call void [mscorlib]System.Console::WriteLine(string)
    IL_005a: nop
    IL_005b: newobj instance void class [mscorlib]System.Collections.Generic.Dictionary`2<stringstring>::.ctor()
    IL_0060: stloc.2
    IL_0061: ldloc.2
    IL_0062: ldstr "a"
    IL_0067: ldstr "1"
    IL_006c: callvirt instance void class [mscorlib]System.Collections.Generic.Dictionary`2<stringstring>::set_Item(!0, !1)
    IL_0071: nop
    IL_0072: ldloc.2
    IL_0073: ldstr "a"
    IL_0078: ldstr "2"
    IL_007d: callvirt instance void class [mscorlib]System.Collections.Generic.Dictionary`2<stringstring>::set_Item(!0, !1)
    IL_0082: nop
    IL_0083: ldloc.2
    IL_0084: stloc.1
    IL_0085: ldstr "Keys : {0}, Values : {1}"
    IL_008a: ldloc.1
    IL_008b: callvirt instance class [mscorlib]System.Collections.Generic.Dictionary`2/KeyCollection<!0, !1> class [mscorlib]System.Collections.Generic.Dictionary`2<stringstring>::get_Keys()
    IL_0090: callvirt instance int32 class [mscorlib]System.Collections.Generic.Dictionary`2/KeyCollection<stringstring>::get_Count()
    IL_0095: box [mscorlib]System.Int32
    IL_009a: ldloc.1
    IL_009b: callvirt instance class [mscorlib]System.Collections.Generic.Dictionary`2/ValueCollection<!0, !1> class [mscorlib]System.Collections.Generic.Dictionary`2<stringstring>::get_Values()
    IL_00a0: callvirt instance int32 class [mscorlib]System.Collections.Generic.Dictionary`2/ValueCollection<stringstring>::get_Count()
    IL_00a5: box [mscorlib]System.Int32
    IL_00aa: call string [mscorlib]System.String::Format(stringobjectobject)
    IL_00af: call void [mscorlib]System.Console::WriteLine(string)
    IL_00b4: nop
    IL_00b5: ret
// end of method Program::DictionaryInitilize
Посмотрите внимательно на этот код. Он не так страшен, как кажется на первый взгляд. Для словаря a используется для добавления значения в словарь метод Add, который пытается вставить две записи с помощью метода Insert. Для словаря b мы же сразу пытаемся проставить нужное значение. Приведенный выше код интерпретируется таксловно мы переписали использование словаря b в следующий вид:
var b = new Dictionary<string, string>();
b["a"] = "1";
b["a"] = "2";
Как видите, мы попросту заменяем наше значение в ключе “a”. Вы сами можете убедиться в том, что именно в такую запись компилятор перегоняет ваш код. Помните, мы в начале рассмотрели пример инициализации словаря _persons. Посмотрите, как компилятор интерпретировал этот код.
public Dictionary<intstring> _persons;
public Program()
{
    Dictionary<intstring> dictionary = new Dictionary<intstring>();
    dictionary[1] = "Aleksand";
    dictionary[2] = "John";
    dictionary[3] = "Petr";
    this._persons = dictionary;
    base..ctor();
}
Теперь вы знаете, что происходит внутри и какое значение будет присвоено ключу “a” для словаря b.
Null Propagation Operator (Null Coalescing Operator)
Как по мне, то это самая замечательная возможность, которая появилась в C# 6-ом. По сути, у вас монада Maybe, которая записывается с помощью оператора ”?.”. Этот оператор нужен для того чтобы обойти ситуацию, когда свойство может быть null. Ниже приведен пример, как вы писали код для более старой версии C# и который вы наверняка использовали с своих программах на 90%.
public event Action<int> Progress;

public void UpdateProgress(int number)
{
    if(Progress != null)
        Progress(number);
}
Хотя вы могли и обходить такую ситуацию с проверкой на null через создание пустого делегата.
public event Action<int> Progress = delegate {};

public void UpdateProgress(int number)
{
    Progress(number);
}
С простыми ивентами это дело десятое. Но если вы пишете на WPF, то вы не могли обойти стороной интерфейс INotifyPropertyChanged с событием PropertyChanged. Посмотрите, если у вас есть использование PropertyChanged в котором вы его проверяете на Null, то можете забыть об этом геморрое. Сейчас достаточно переписать приведенный выше пример с использованием null coalescing operator.
public event Action<int> Progress;

public void UpdateProgress(int number)
{
    Progress?.Invoke(number);
}
Второе место, где удобно использовать данный оператор — это метод Dispose, в котором мы зачастую проверяем, не равен ли объект Null, и если он все-таки не равен, то вызвать для этого объекта Dispose.
Before:
public Point _point;
public void Dispose()
{
    if(_point != null)
        _point.Dispose();
}
After:
public Point _point;
public void Dispose()
{
    _point?.Dispose();
}
Есть и другие применения данного оператора, как, например, большая вложенность, когда каждый член может быть равен null. У меня такой код очень редко встречался поэтому не хочется даже приводить штучный пример для него. Ну и, наверное, этот оператор неплохо вам может пригодиться совместно с LINQ, если вы будете использовать функции, которые возвращают какое-то дефолтное значение FirstOrDefault, SingleOrDefault и т.д.

Заключение
Мы рассмотрели примеры, которые зачастую встречаются в реальных проектах, а также показал, как та или иная особенность работает изнутри. Эта часть включает все оставшееся возможности языка C#, не описанные в таких моих предыдущих статьях, как "Expression body members and auto-properties in C# 6.0", "Filter exceptions" и "Async/await in catch/finally blocks". Все, что вас интересует, вы можете найти в предыдущих статьях. Надеюсь, что статья получилась не очень сухой и понравилась вам приведенными примерами. В целом, у меня по планах осталась одна статья, которая предполагает рассмотреть C# 6.0 с критической точки зрения и описать те или иные возможности в плане полезности. Буду благодарен за ваши комментарии по поводу изложения и подачи материала.  

No comments:

Post a Comment