Tuesday, May 12, 2015

C# 6 и синтаксический сахар

Сегодня попрактикуемся обнаруживать признаки "синтаксического сахара" в языке C# 6. Синтаксический сахар (англ. syntactic sugar) – дополнения синтаксиса языка программирования, которые не добавляют новых возможностей, а делают использование языка более удобным для человека. 
Вы, наверное, слышали выступления докладчиков о новых возможностях языка C# 6 и его крутости. К началу апреля 2015 года появился релиз версии-кандидата Visual Studio 2015 с поддержкой языка C# 6.0. Но к сожалению, новых возможностей в языке C#, мягко говоря, негусто. 
Первое новшествоиспользование null propagation operator, async/await для блоков catch/finally и filter exceptions. Сложно сказать, что фильтры исключений – какая-то сверхполезная особенность языка C#; это выглядит больше как расширение уже существующих возможностей, чем добавление чего-то нового. Async/await для блоков catch/finally действительно не хватало, но если честно, я не очень часто видел код, в котором было бы круто использовать await для блока catch или finally. Я все время следил за тем, как C# 6 развивался, скачивал Visual Studio CTP, которая с самого начала называлась Visual Studio "14". Много задуманных возможностей чем ближе к релизу, тем быстрее выбросились из-за некоторых проблем с их имплементацией. Теперь эти возможности анонсируют как возможности языка C# 7. Например, недавно анонсировали, что в C# 7 будет добавлена возможность создания кортежей динамически. Что-то вроде следующего:
var tuple = (5, 5);
var t = new (int sum, int count) { sum = 0, count = 0 };
Или даже функции:
public (int sum, int count) Tally(IEnumerable<int> values)
{
    var res = (sum: 0, count: 0); // Заполнение данных прямо во время создания анонимной структуры
    foreach (var value in values) { res.sum += value; res.count++; }
    return res;
}
Пример функции выше взят с habrahabr "Что нам готовит C# 7" (Часть 1. Кортежи). Можно по-разному относиться к этой новости про кортежи, вот только при начале обсуждения возможностей языка C# 6 кортежи были чуть ли не на первом месте по обсуждениям, а потом их просто убрали. Поэтому мы получим этот синтаксический сахар уже в новой версии C#. А пока давайте вернемся к таковому в C# 6.
Expression body members
На новом C# 6.0 появилась возможность использования лямбда-выражений для задания тела функций и свойств. Давайте посмотрим на практике, как это выглядит.
public static int Sum(int x, int y) => x + y;
На самом деле это ничто иное, как другая форма записи функции.
public static int Sum(int x, int y)
{
    return x + y;
}
Никаких делегатов IL не генерирует. Генерируется действительно запись так, словно вы не использовали выражения вида "=>". Вы можете посмотреть на IL-код, чтобы самим в этом убедиться.
Кстати, для свойств запись выглядит аналогичным образом:
public int A => 5;
Что эквивалентно в более ранней версии записи типа:
public int A
{
    get { return 5; }
}
Только если вы обратите внимание на то, как компилятор приводит ваше выражение в IL-код, то вы можете увидеть, что запись =>, по сути, является преобразованием ваших скобок {}.
Read-only automatically implemented properties
В C# 6.0 появилась удобная короткая форма записи для read-only автопропертей.
public int X { get; } = 5;
Чтобы не томить вас тем, что происходит внутри, скажу, что на уровне IL-кода у вас будет сгенерирована переменная в формате <Имя свойства> k_BackingField, и сама переменная будет присвоена в конструкторе по умолчанию.
. field private initonly
Это одна из самых главных особенностей C# 6.0 для readonly autoproperty.
Более детально остановимся на том, как в конструкторе происходит присвоение, когда рассмотрим инициализацию начальных значений для autoproperties, так как логика у них практически идентичная.
Initial values for auto-properties
Разницы с предыдущей формой записи практически нет; отличие только в том, что это не readonly поле. Так как в документации Microsoft разделяет эти два способа записи (хотя если честно, то не пойму зачем), мы тоже рассмотрим их по отдельности. Так вот, в общем отличия readonly automatically implemented properties, кроме того, что здесь поле не readonly, больше и нет. Смотрите сами:
public int Y { get; set; } = Sum(1,2);

public int V { get; private  set; } = Sum(5,7);
ФункцияSum уже приводилась выше, но на всякий случай, повторю ее здесь.
public static int Sum(int x, int y) => x + y;
Давайте ниже рассмотрим, как выглядит свойство Y на уровне IL-кода.
property instance int32 V()
{
    .get instance int32 ExpressionBodySample.Program::get_V()
    .set instance void ExpressionBodySample.Program::set_V(int32)
}

.method public hidebysig specialname
    instance int32 get_V () cil managed
{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    // Method begins at RVA 0x2100
    // Code size 11 (0xb)
    .maxstack 1
    .locals init (
        [0] int32
    )

    IL_0000: ldarg.0
    IL_0001: ldfld int32 ExpressionBodySample.Program::'<V>k__BackingField'
    IL_0006: stloc.0
    IL_0007: br.s IL_0009

    IL_0009: ldloc.0
    IL_000a: ret
// end of method Program::get_V

.method private hidebysig specialname
    instance void set_V (
        int32 'value'
    ) cil managed
{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    // Method begins at RVA 0x2117
    // Code size 10 (0xa)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldarg.1
    IL_0002: stfld int32 ExpressionBodySample.Program::'<V>k__BackingField'
    IL_0007: br.s IL_0009

    IL_0009: ret
// end of method Program::set_V
Выше приведены классические автосвойства, которые вы использовали наверняка неоднократно в своем коде.  Если вы забыли, как выглядят обычные автосвойства, для вас они в примере ниже:
public int A {get; set;}
Проставление значению V и Y будет произведено в конструкторе, что можно увидеть на рисунке ниже.
Ничего особенного в рассмотренных трех фишках нового языка, по сути, мы не увидели. Это просто расширения старых возможностей.
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; }
}
И как это выглядит сейчас:
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);
}
Если вы думаете, что там происходит что-то сверхъестественное, то вы заблуждаетесь. В 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
Это расширение синтаксиса языка делает его чуточку удобнее, чем это было раньше. 
Interpolation string
Это новая особенность есть не что, иное как синтаксический сахар, и представляет нам более удобную форму записи string.Format. Давайте посмотрим на то, как это выглядит в новом C#.
var point = new Point { X = 3, Y = 5 };
var message = $"coordinate X = {point.X}, coordinate Y = {point.Y}";
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
Есть, правда, и плохие новости по поводу удобства этой записи. Так, пример, который я приведу ниже, сложно переписать на новый синтаксис.
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);
}
Выглядеть это будет просто очень страшно.
Using static members
Теперь для статических функций и свойств мы можем писать более короткий и лаконичный код. Ниже приведен пример использования пространства имен Syste.Console.
using static System.Console;
static void Main(string[] args)
{
    var point = new Point { X = 3, Y = 5 };
    var message = $"coordinate X = {point.X}, coordinate Y = {point.Y}";
    WriteLine(message);

    ReadLine();
}
У нас появилась новая запись using static, которую мы можем использовать для наших статических классов. Это как минимум позволяет делать наш код чище и читабельнее, но мне не нравится использование данного подхода совместно с Linq. Выглядит такой код, как минимум, странно, возможно из-за непривычки. По сути, мы рассматриваем уже 6-ую мелкую возможность, которая является расширением старой версии языка и делает ее более удобной.
Dictionary initializer
Если быть кратким, то для словарей появилась возможность писать более простой для чтения код, пример которого можно увидеть ниже.
Before:
public Dictionary<int, string> _persons = new Dictionary<int, string>
{
    {1, "Aleksand"},
    {2, "John"},
    {3, "Petr" }
};
After:
public Dictionary<int, string> _persons = new Dictionary<int, string>
{
    [1] = "Aleksand",
    [2] = "John",
    [3] = "Petr"
};
Запись внизу интерпретируется компилятором в запись вида:
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();
}
Поэтому будьте готовы, если случайно укажете дважды один и тот же ключ, и это значение у вас будет перезаписано, так как в старом варианте вы бы получили ошибку, что добавили один и тот же ключ дважды.

Заключение
Из не упомянутых в статье возможностей, которые добавились в язык C#, остается  null propagation operator. Фильтры исключения создают интересную тему, которую я описал в статье "Filter exceptions in C# 6.0", но практического применения ей мне придумать не удалось. Не думаю, что фильтры исключений займут какую-то особенную нишу при написании прикладных программ. Использование await вообще выглядит как попытка заткнуть дыру, в которой была промашка.
Мне понравилось, как об этом всем высказался Джон Скит: “What a lot of features! C# 6 is definitely a “lots of little features” release rather than the “single big feature” releases we’ve seen with C# 4 (dynamic) and C# 5 (async). Even C# 3 had a lot of little features which all served the common purpose of LINQ. If you had to put a single theme around C# 6, it would probably be making existing code more concise – it’s the sort of feature set that really lends itself to this “refactor the whole codebase to tidy it up” approach.

В целом, возможно,неплохо добавить много мелких возможностей, вместо одной мощной. Хотя с другой стороны в этом может быть и какой-то плюс, который, к сожалению, не виден невооруженным взглядом, кроме как изменение синтаксиса, использование которого местами стало не проще, чем было до этого. Как видите, можно пропиарить новую версию языка, словно это куча новых фишек, как это сделали Microsoft. И вуаля, у вас новый язык, который называется C# 6. Мне нравится язык C#, но признаться честно, несколько разочарован его новой версией. Плюшек-то, по большому счету, только несколько, поэтому можно писать спокойно так, как и раньше, код на C# 5, без какого-то либо дискомфорта, потому что вы ничего не потеряете. 

No comments:

Post a Comment