Сегодня попрактикуемся обнаруживать признаки "синтаксического сахара" в
языке 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
{
.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(string, object, object)
IL_0034: call void [mscorlib]System.Console::WriteLine(string)
IL_0039: nop
IL_003a: ret
} // end of method Program::Print
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(string, object, object)
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(string, object, object)
IL_0039: stloc.1
IL_003a: ldloc.1
IL_003b: call void [mscorlib]System.Console::WriteLine(string)
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(string, object, object)
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
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.
Теперь для статических функций и свойств мы можем писать более короткий и лаконичный код. Ниже приведен пример использования пространства имен 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<int, string> _persons;
public Program()
{
Dictionary<int, string> dictionary = new Dictionary<int, string>();
dictionary[1] = "Aleksand";
dictionary[2] = "John";
dictionary[3] = "Petr";
this._persons = dictionary;
base..ctor();
}
{
Dictionary<int, string> dictionary = new Dictionary<int, string>();
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