Thursday, April 23, 2015

Expression body members and auto-properties in C# 6.0

Сегодня я расскажу о нескольких интересных новых возможностях языка C# 6.0. Мы рассмотрим всего несколько из них и посмотрим на то, как эти возможности работают изнутри IL-кода. На повестке дня у нас expression body members, read-only automatically implemented properties ('auto-props'), простановка значений для automatically implemented properties
Начнем, пожалуй, с рассмотрения expression body members.
На новом C# 6.0 появилась возможность использования лямбда-выражений для задания тела функций и свойств. Давайте посмотрим на практике, как это выглядит.
public static int Sum(int x, int y) => x + y;

public static int Sum2(int x, int y)
{
    return x + y;
}
Ниже приведены две функции, Sum и Sum2. Запись функции Sum была характерна для версии C# ниже 6-ой версии. И более новая запись, которая представлена функцией Sum2, актуальной записью для 6-го C#. 
Мне нравится смотреть, как работает весь такой код изнутри, с помощью ILSpy или .NET Reflector, чтобы вникнуть во всю подноготную того, что нового "напилили" в Microsoft, и не является ли новая возможность очередным синтаксическим "сахаром".
Функция Sum:
Функция Sum2:
Как видим, функции одинаковые. Запись с лямбда-выражением для свойств выглядит следующим образом.
public int A => 5;
Что эквивалентно в более ранней версии записи типа:
public int A
{
    get { return 5; }
}
Только если вы обратите внимание на то, как компилятор приводит ваше выражение в IL-кодвы можете увидеть, что запись => является, по сути, преобразованием ваших скобок {}. Компилятор не создает никаких делегатов для данных целей, поэтому называть данную запись как lambda expression, наверное, не совсем корректно. Поскольку это просто новый способ записи одной и той же вещи. Признаюсь честно, что до тех пор, пока не посмотрел, во что компилируется данное выражение, я думал, что компилятор внутри клепает делегаты и что можно сделать для таких функций замыкание на переменных. Как хорошо, что я ошибался. В целом новая возможность языка C# очень интересная и как по мне, точно найдет свое применение в сфере разработки программ на языке C#, вот только в плане читабельности я не уверен, что этот код будет читаться лучше, чем это было в C# 5.0 и ниже. Хотя, как и в каждом языке, здесь главное – не злоупотреблять хорошими вещами.
Read-only automatically implemented properties
В C# 6.0 появилась удобная короткая форма записи для read-only автопроперти. Вот как можно создать свойство X, которое будет возвращать значение 5.
public int X { get; } = 5;
До этого в C# 5.0 и ниже нам нужно было написать код, подобный этому:
private int _x = 5;
public int X { get return _x; }
По умолчанию для C# 5.0 мы не могли создать проперти с одним get.
Если мы посмотрим на уровне IL-кода, то увидим, что для нашего свойства X была создана внутренняя переменная <X>k_BackingField.
.field private initonly int32 '<X>k__BackingField'
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
    01 00 00 00
)
.custom instance void [mscorlib]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [mscorlib]System.Diagnostics.DebuggerBrowsableState) = (
    01 00 00 00 00 00 00 00
)
Посмотрите внимательно на запись. field private initonly
Это одно из самых главных особенностей C# 6.0 для readonly autoproperty. По сути компилятор за вас создаст сам необходимую переменную. Выглядит это больше как новый синтаксический "сахар" для языка C#, чем какая-то особенная возможность.
Initial values for auto-properties
Более интересной, на мой взгляд, является не так установка значений для неизменяемых свойств (read-only auto-properties), как установка значений для автосвойств, которые могут быть изменены. Ниже приведен пример использования таких свойств.
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
Раскрою секрет, что точно так же выглядит использование auto-properties C# 5.0. Например, для записи типа
public int P { get; set; }
будет сгенерирован аналогичный IL-код.
.property instance int32 P()
{
    .get instance int32 ExceptionFilters.Program::get_P()
    .set instance void ExceptionFilters.Program::set_P(int32)
}

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

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

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

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

    IL_0000: ldarg.0
    IL_0001: ldarg.1
    IL_0002: stfld int32 ExceptionFilters.Program::'<P>k__BackingField'
    IL_0007: ret
// end of method Program::set_P
А хитрость вот в чем. Посмотрите внимательно на рисунок ниже, и вы сами поймете, в чем подвох.
На рисунке выше показан сгенерированный по умолчанию конструктов, в котором для наших auto-properties Y и V проставлено начальное значение. На уровне IL-кода это выглядит следующим образом:
.method public hidebysig specialname rtspecialname
    instance void .ctor (
        int32 c
    ) cil managed
{
    // Method begins at RVA 0x2050
    // Code size 56 (0x38)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldc.i4.5
    IL_0002: stfld int32 ExpressionBodySample.Program::'<X>k__BackingField'
    IL_0007: ldarg.0
    IL_0008: ldc.i4.1
    IL_0009: ldc.i4.2
    IL_000a: call int32 ExpressionBodySample.Program::Sum(int32int32)
    IL_000f: stfld int32 ExpressionBodySample.Program::'<Y>k__BackingField'
    IL_0014: ldarg.0
    IL_0015: ldc.i4.5
    IL_0016: ldc.i4.7
    IL_0017: call int32 ExpressionBodySample.Program::Sum(int32int32)
    IL_001c: stfld int32 ExpressionBodySample.Program::'<V>k__BackingField'
    IL_0021: ldarg.0
    IL_0022: ldc.i4.5
    IL_0023: stfld int32 ExpressionBodySample.Program::p
    IL_0028: ldarg.0
    IL_0029: call instance void [mscorlib]System.Object::.ctor()
    IL_002e: nop
    IL_002f: nop
    IL_0030: ldarg.0
    IL_0031: ldarg.1
    IL_0032: stfld int32 ExpressionBodySample.Program::'<C>k__BackingField'
    IL_0037: ret
// end of method Program::.ctor
А на уровне кода C# это интерпретируется в строки следующего вида.
// ExpressionBodySample.Program
[DebuggerBrowsable(DebuggerBrowsableState.Never), CompilerGenerated]
private readonly int <X>k__BackingField = 5;
[DebuggerBrowsable(DebuggerBrowsableState.Never), CompilerGenerated]
private int <Y>k__BackingField = Program.Sum(12);
[DebuggerBrowsable(DebuggerBrowsableState.Never), CompilerGenerated]
private int <V>k__BackingField = Program.Sum(57);
private int p = 5;
public Program(int c)
{
    this.<C>k__BackingField = c;
}
Выглядит это довольно забавно, и по сути, немного упрощает работу разработчиков.

Итоги
Рассмотренные в статье возможности можно больше отнести к синтаксическому сахару языка C# 6.0, чем к каким-то новым возможностям. К этой статье, наверное, можно было бы добавить также использование string interpolation (интерполяцию строк), в которой вместо записи String.Format, новый C# предоставляет более простой и лаконичный синтаксис (который на внутреннем уровне интерпретируется в той же самый старый и добрый string.Format), но эту возможность я решил оставить для следующей статьи. К сожалению, а может и к счастью, новых возможностей в C# 6.0 очень мало. В основном это синтаксическая обертка для уже существующего функционала. К новым возможностям можно отнести использование await в блоках catch/finally, о котором я написал статью 'Async и await в C# 6.0', фильтры исключений 'Filter exceptionsin C# 6.0', и две возможности, которые мы еще не рассмотрели: nameof оператор и null-conditional operator (реализация монады maybe на уровне C# оператора .?). 
Инициализация dictionarystring interpolationusing static members и т.д. – это все обертки над тем, что уже существует. По сути, единственное новое, что может сломать вам обратную совместимость в некоторых случаях,  это добавление для структур конструктора без параметров, о чем мы поговорим в следующих статьях. К сожалению, компания Microsoft вложила уйму сил в Roslyn, в то чтобы сделать все открытым, но времени для того, чтобы внести что-то действительно стоящее в сам язык, увы, не хватило. Хотя, возможно в данном плане я слишком придирчив. Спасибо, что потратили свое время на ознакомление с новыми возможностями языка C# 6.0. Далее мы разберем что-то новое в C#, так что до встречи в следующих статях. 

No comments:

Post a Comment