Сегодня я расскажу о нескольких интересных новых
возможностях языка 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, и не является ли новая возможность очередным синтаксическим "сахаром".
Мне нравится смотреть, как работает весь такой код изнутри, с помощью 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
)
.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
{
.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
{
.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(int32, int32)
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(int32, int32)
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
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(int32, int32)
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(int32, int32)
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(1, 2);
[DebuggerBrowsable(DebuggerBrowsableState.Never), CompilerGenerated]
private int <V>k__BackingField = Program.Sum(5, 7);
private int p = 5;
public Program(int c)
{
this.<C>k__BackingField = c;
}
[DebuggerBrowsable(DebuggerBrowsableState.Never), CompilerGenerated]
private readonly int <X>k__BackingField = 5;
[DebuggerBrowsable(DebuggerBrowsableState.Never), CompilerGenerated]
private int <Y>k__BackingField = Program.Sum(1, 2);
[DebuggerBrowsable(DebuggerBrowsableState.Never), CompilerGenerated]
private int <V>k__BackingField = Program.Sum(5, 7);
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# оператора .?).
Инициализация dictionary, string interpolation, using static members и т.д. – это
все обертки над тем, что уже существует. По сути, единственное новое, что
может сломать вам обратную совместимость в некоторых случаях, – это добавление для
структур конструктора без параметров, о чем мы поговорим в следующих
статьях. К сожалению, компания Microsoft вложила
уйму сил в Roslyn, в то чтобы
сделать все открытым, но времени для того, чтобы внести что-то действительно
стоящее в сам язык, увы, не хватило. Хотя, возможно в данном плане я слишком
придирчив. Спасибо, что потратили свое время на ознакомление с новыми
возможностями языка C# 6.0. Далее мы разберем что-то новое в C#, так что до встречи в следующих статях.
No comments:
Post a Comment