Почему-то мы зачастую не используем этот функционал. Может быть еще не успели к нему привыкнуть. А иногда используем, при этом не имея представления, что это функционал из F#.
Прежде чем перейти к его рассмотрению, давайте стремительно пробежимся
Ретроспектива
C# 1.0 Visual Studio 2002
C# 1.1 Visual Studio 2003 — #line, pragma, xml doc comments
C# 2.0 Visual Studio 2005 — Generics, Anonymous methods, iterators/yield, static classes
C# 3.0 Visual Studio 2008 — LINQ, Lambda Expressions, Implicit typing, Extension methods
C# 4.0 Visual Studio 2010 — dynamic, Optional parameters and named arguments
C# 5.0 Visual Studio 2012 — async/await, Caller Information, some breaking changes
C# 6.0 Visual Studio 2015 — Null-conditional operators, String Interpolation
C# 7.0 Visual Studio 2017 — Tuples, Pattern matching, Local functions
Некоторый функционал довольно редко используется, но что-то используется постоянно. Скажем, даже сейчас довольно часто все еще можно встретить использование OnPropertyChanged с указанием имени свойства. То есть что-то вроде OnPropertyChanged(«Price»); Хотя уже с 5-ой версии языка стало возможным получить имя вызываемого объекта с помощью CallerMemberName.
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string prop = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
Таким же образом можно организовать логирование. Получить имя метода из которого происходит вызов поможет все тот же атрибут CallerMemberName. Таким же образом можно получить имя файла и номер строки с помощью [CallerFilePath] и [CallerLineNumber]. Кстати, эти атрибуты можно использовать и в F#, но речь не о них.
Говоря о новых возможностях, которые появились в C#, в последнее время никак нельзя не упомянуть «интервенцию» F#, которая началась, начиная с 6-ой версии языка. Все началось со всеми любимого LINQ. Вот перечисление только некоторых возможностей, которые появились в C#: LINQ, иммутабельность, Exception filters, Auto-property initializers, Expression-bodied function members, Pattern matching, Tuples
Судя по всему, даже если вы не начнете в ближайшее время изучать F#, то скоро функциональное программирование станет вам чуть-чуть знакомым. Давайте рассмотрим несколько «функциональных» возможностей C#.
Иммутабельность
Это ни что иное, как неизменяемость объектов. То есть, значение объекта не может быть изменено после создания. В F# все переменные по умолчанию неизменяемы. Каким образом это может быть реализовано в C#? Начиная с 6-ой версии можно создать read-only свойства не указывая set. Например, так:
public string Name { get;}
Таким образом становится возможным комфортным образом создавать свои иммутабельные объекты.
Exception filters
А это возможность при «ловле» ошибок указать параметр, при котором сработает захват. Например, так:
try
{
SomeMethod(param);
}
catch (Exception e) when (param == null)
{
}
catch (Exception e)
{
}
В первом блоке ошибка будет отловлена только в том случае, если param==null. Во второй попадут ошибки, возникшие при значениях param отличных от null.
Auto-property initializers
Это возможность инициализировать свойство сразу после методов доступа (аксессоров). Например, так:
public string AppUrl { get; set; } = "http://lalala.com";
Или можно даже вынести инициализацию в отдельный метод, возвращающий значение.
public string AppUrl { get; set; } = InitializeProperty();
public static string InitializeProperty()
{
return "http://lalala.com ";
}
Expression-bodied function members
Удобная возможность сократить код с помощью лямбда выражений. Методы, которые раньше записывались так:
public int Sum(int x, int y)
{
return x+y;
}
public string ServerIP { get { return "65.23.135.201"; } }
теперь можно записать гораздо короче:
public int Sum(int x, int y) => x+y;
public string ServerIP => "65.23.135.201";
Все эти только что перечисленные возможности появились в 6-ой версии C#.
Теперь давайте разберем чуть более подробно то, что пришло из F# в 7-ой версии языка.
Кортежи или Tuples
Этот функционал по мнению многих может сравнится с LINQ по степени важности и удобности использования.
Что по себе он нам дает. В первую очередь это возможность вернуть из метода несколько значений, не создавая при этом экземпляр какого-либо класса. Потому как передать в метод мы можем несколько параметров, а вот вернуть получится только что-то одно. Повторюсь, что типичным решением для данной потребности до сих пор было создание экземпляра класса. Сейчас же мы можем создать кортеж.
Типичный пример кортежа:
var unnamed = (35, "What is your age?");
Как вы можете заметить это ни что иное как переменная, которая содержит в скобках два значения. В данном случае кортеж называется неименованным и к значениям можно обратится по имени Item с номером. Например, unnamed.Item1 содержит в себе 35, а unnamed.Item2 строку с текстом «What is your age?»
Если кто-то заметит, что кортежи похожи на anonymous types, то будет прав. Но есть нюанс, о котором не стоит забывать. Anonymous types могут использоваться только в области видимости (scope) метода.
Бывают именованные кортежи.
var named = (Answer: 35, Question: "What’s your age again?");
К переменным из именованного кортежа можно обратится по именам. В данном случае named.Answer хранит 35, а named.Question строку с текстом вопроса.
Простейший пример. Метод, который возвращает значение в виде кортежа:
(int, string) GetXY()
{
int x = 1;
string y = "One";
return (x, y);
}
Возвращаемым значением метода является кортеж, первым значением которого является целое число типа int, а вторым строка.
Получаем значение в переменную так:
var xy = GetXY();
Теперь можем обращаться к элементам кортежа по xy.Item1 и xy.Item2
У кортежей есть такая интересная возможность как deconstruction/деконструкция. Это получение обычными переменными значений из кортежа. Ну или можно сказать разложение кортежа на отдельные переменные. Например, есть такой вот метод, возвращающий список и какой-то ID-шник, относящийся к данному списку:
(int, List<string>) GetListWithId()
{
int x = 1;
List<string> y = new List<string>();
return (x, y);
}
Для того, чтобы получить значения сразу в виде переменных, а не в виде кортежа, можно использовать один из двух следующих способов:
(int x, List<string> y) = GetListWithId();
var (x, y) = GetXY();
В C# версии 7.1 появилась возможность под названием infer tuple names. На русский можно перевести примерно, как: предполагаемые имена кортежей. Это значит, что можно будет обратиться к неименованному элементу кортежа не только по Item1, Item2 и т.д., но и по имени, которое формируется исходя из названия переменной принимавшей участие в создании кортежа. Пример:
var tuple = (x.a, y);
К элементам этого кортежа можно обратиться по именам tuple.a и tuple.y
Что интересно, так это то, что это изменение 7.1 является breaking back compatibility. То есть обратно несовместимым. Но так как промежуток между выходом C# 7 и C# 7.1 небольшой, то его решили внести. Суть в чем. Какой-то код, который работал определенным образом в C# 7 будет работать иначе в C# 7.1.
В данном случае возможность встретить такой код в реальном проекте чрезвычайно мала.
Пример. Допустим у вас есть такой код:
int x=1;
Action y = () => SomeMethod();
var tuple = (a: x, y);
tuple.y();
Обратите внимание на последнюю строчку, которая очевидно вызывает метод с именем SomeMethod (и она действительно вызывает этот метод, но только начиная с C# 7.1)
Так вот. В C# 7.0 был бы вызван не SomeMethod, а extension метод с именем y. Допустим, такой:
public static class ExtClass
{
public static void y(this (int, Action) z)
{
// some code
}
}
Согласитесь, что extension методы для tuples это редкость.
Если вы используете проект с версией .NET Framework ниже чем 4.7, то вы сможете использовать кортежи только установив NuGet пакет System.ValueTuple. Впрочем, Visual Studio 2017 должна сама вам подсказать это, если вы начнете использовать синтаксис кортежей. Может быть поэтому кортежи пока что все еще не так часто используются. Необходимо не только работать в последней версии Visual Studio, но и на одной из последних версий фреймворка (или устанавливать пакет NuGet).
Pattern Matching
Под этим довольно неоднозначным названием содержится на удивление довольно простой функционал.
Давайте рассмотрим пример кода, который использовался ранее и, конечно же, может использоваться сейчас:
if (someObject is Customer)
{
var c = (Customer)someObject;
c.Balance = c.Balance + 1000;
}
Код совершенно нормальный, типичный, часто используемый и нельзя сказать, что в нем что-то не так. Однако, теперь появилась возможность его немного сократить:
if (someObject is Customer c) c.Balance = c.Balance + 1000;
Получается, что если someObject относится к классу Customer, то он преобразуется в переменную с типа Customer. И с этой переменной сразу можно начинать работать.
Можно добавить, что pattern matching это не только синтаксический сахар, но и возможность разработчикам имевших дело с подобными конструкциями в других языках использовать их в C#.
Pattern matching можно использовать не только с if, но и с конструкциями Swith. Пример:
switch (userRole)
{
case Manager m:
return m.Salary;
case Partner p:
return p.Income;
}
Кроме того, можно делать уточнения с помощью условий with
switch (userRole)
{
case Manager m with Salary<1500:
return m.Salary*1.2;
case Manager m:
return m.Salary;
case Partner p:
return p.Income;
}
В данном случае первый case будет выполнен только если userRole это менеджер и значение его Salary меньше 1500. То есть m.Salary<1500
В списке предполагаемых нововведений в C# 8 есть функционал, который не так давно появился в Java. А именно: методы интерфейсов по умолчанию (default interface methods). В частности, с помощью данного функционала можно будет изменять legacy-код. Например, самим разработчикам Java удалось с помощью данного функционала улучшить API коллекций и добавить поддержку лямбда выражений. Может быть и разработчики C# тоже хотят что-то изменить в самом C# с помощью этого функционала?
Не смотря на появившееся сходство, интерфейсы все еще довольно отличаются от абстрактных классов. От множественного наследования в C# отказались давно из-за множества возникающих сложностей. Но в данном случае, используя только один метод, сложностей должно оказаться меньше. В случае, если класс наследует от двух или большего количества интерфейсов и в нескольких интерфейсах присутствует метод с одним и тем же названием, то класс должен обязательно указать имя метода с уточнением имени интерфейса. Отследить только один метод проще (в частности и с помощью IDE).
Язык C# используется в довольно большом количество различных типов проектов. Несмотря на то, что он не занимает первую строчку по популярности, он уникален шириной охвата типов проектов. Веб-разработка, десктопная, кроссплатформенная, мобильная, игры… Включая в себя какие-то возможности функционального программирования или других развивающихся параллельно языков C# становится более универсальным. Перейти на C# из другого языка становится легче. Впрочем, и опытным разработчикам C# в свою очередь становится проще понимать синтаксис других языков.