В этой статье будут рассмотрены с нового ракурса такие привычные и фундаментальные вещи, как присваивание и передача параметров в методы.
Вероятно, предлагаемые решения поначалу покажутся несколько странными и надуманными, но прелесть их раскроется чуть позже, когда станет видна вся картина целиком.
Будет много нового и интересного, возможно, даже полезного. А после прочтения каждый сам сможет решить, стоит ли ему применять описанные техники в дальнейшей повседневной практике.
За дело!

Существует два направления присваивания: правое и левое
Да, все методы с `out` и частично с `ref` параметрами являются вариациями правостороннего присваивания.
С ранних версий C# поддерживает `out` и `ref` параметры, что даёт некоторые преимущества, но не очень впечатляющие, однако C# 7 совершил эволюционный скачок!
Добавление синтаксического сахара вроде `o.To(out var x)` позволило объединить правостороннее присваивание вместе с декларацией переменной, что дало возможность обобщить и уточнить некоторые распространённые сценарии в программировании…
Исторически более привычной является традиционная левостронняя ориентация при присваивании. Возможно, это влияние математики, где `y = f(x)` является стандартной нотацией. Но на практике в программировании такое положение вещей вызывает некоторые ограничения (будут упомянуты далее) и неудобства, например, визуальный переизбыток скобок ('parentheses hell') при цепочном привидении типов для урегулирования приоритетов
что подталкивает разработчиков к использованию многословных либо плохих решений наподобие
Тем не менее существует менее очевидное, но более элегантное решение проблемы путём правостороннего приведения типа
При дальнейшем обобщении подхода мы получаем следующий набор методов-расширений
которые позволяют отзеркалить направление всех трёх базовых операций: декларации переменной, привидения типа и присваивания значения
Эти примеры иллюстрируют, что исторически C# потерял что-то вроде оператора `to`. Сравните
Многим разработчикам хорошо знакомы инициализационные блоки в духе `json`
вместо
Они довольно хороши и удобны, поскольку позволяют структурировать код и сделать его более чистым и читаемым. Однако у таких блоков есть недостаток — они применимы лишь в связке с конструктором. Но иногда для создания объектов предпочтительно использование методов-фабрик, и, к сожалению, в таких случаях инициализационные блоки непригодны
Другими словами, простая замена конструктора на метод-фабрику может вызывать кардинальную смену структуры кода. Как этого избежать?
Для начала рассмотрим два метода-расширения
Они позволяют нам переписать код следующими способами
либо
* при желании можно поиграть с примерами в онлайн-компиляторе по ссылке
Это чуть более многословная, но обощённая запись, в сравнении с инициализационными блоками. Немаловажно, что поддерживаются рекурсивные выражения совместно с оператором проверки на `null` (`?`), а также вызовы функциональных методов, возвращающих значения, например,
Однако предложенная реализация метода `With` имеет несколько недостатков:
Эти проблемы могут быть устранены следующим образом
Если же необходимо получить крупное, но хорошо оптимизированное `With` выражение, то допустима конкатенация (склеивание) нескольких более коротких выражений
Данный подход имеет производительность предельно близкую к идеальной.
Существует также неочевидный эффект, связанный со структурами. Для примера, если мы хотим модифицировать структуру и вернуть её в цепочку вызовов методов, то нам необходимо использовать `put`-паттерн
Дело в том, что при вызове метода-расширения для структуры происходит её копирование, в результате чего метод `With` возвращает её оригинал вместо модифицированного экземпляра
Также `With` метод полезен в подобных сценариях
Это базовая информация о `to-with` паттерне, но не вся.
Неочевидная, но очень важная вещь — паттерн двунаправленный и зеркально симметричный относительно присваивания.
Это означает, что мы можем его использовать для инициализации и деконструкции объектов одновременно!
Как видно, обычные `json` подобные инициализационные блоки являются лишь ограниченной (отчасти из-за левостороннего присваивания) частной синтаксической вариацией намного более обобщённого `with` паттерна.
Кроме того, подобный подход применим и для инициализаторов коллекций
где
Возможно также реализовать очень близкий по духу `check` паттерн для условных выражений
Взгляните
Предназначен для смены контекста в цепочке вызовов, а также декларации произвольного значения в любом контексте
Позволяет объявить новую переменную в цепочке вызовов либо выполнить сторонний метод
Предоставляет возможность использовать вывод типов при декларации массивов и коллекций, а также создавать объекты с помощью обобщённого метода
Добавляет поддержку групповой инициализации переменных одним значением, в том числе в процессе декларации с выводом типа
Альтернатива классическому оператору `switch` на основе лямбда-выражений
Детальные реализации и примеры кода находятся по ссылкам
Github mirror: implementation / some tests
Bitbucket mirror: implementation / some tests
Рассмотренные расширения очень помогают при написании `expression-bodied`методов, а также позволяют сделать код более чистым и выразительным. И если ты тоже ощутил вкус этих расширений, то приятного применения на практике!
Подготовка материалов для данной статьи потребовала очень значительных вложений свободного времени и сил, поэтому если ты сочтёшь информацию практически полезной, то можешь выразить свою благодарность в виде добровольного пожертвования произвольного размера или просто поделиться информацией с друзьями и коллегами. Это очень ценно!
Вероятно, предлагаемые решения поначалу покажутся несколько странными и надуманными, но прелесть их раскроется чуть позже, когда станет видна вся картина целиком.
Будет много нового и интересного, возможно, даже полезного. А после прочтения каждый сам сможет решить, стоит ли ему применять описанные техники в дальнейшей повседневной практике.
За дело!

1. Правосторонние операции: присваивание, декларация переменных и приведение типа
Существует два направления присваивания: правое и левое
IModel m; m = GetModel(); // left side assignment GetModel().To(out m); // right side assignment
Да, все методы с `out` и частично с `ref` параметрами являются вариациями правостороннего присваивания.
С ранних версий C# поддерживает `out` и `ref` параметры, что даёт некоторые преимущества, но не очень впечатляющие, однако C# 7 совершил эволюционный скачок!
Добавление синтаксического сахара вроде `o.To(out var x)` позволило объединить правостороннее присваивание вместе с декларацией переменной, что дало возможность обобщить и уточнить некоторые распространённые сценарии в программировании…
Исторически более привычной является традиционная левостронняя ориентация при присваивании. Возможно, это влияние математики, где `y = f(x)` является стандартной нотацией. Но на практике в программировании такое положение вещей вызывает некоторые ограничения (будут упомянуты далее) и неудобства, например, визуальный переизбыток скобок ('parentheses hell') при цепочном привидении типов для урегулирования приоритетов
public void EventHandler(object sender, EventArgs args) => ((IModel) ((Button) sender).DataContext).Update(); // in a general case there is not possible settle priorities without parentheses // (IModel) (Button) sender.DataContext.Update();
что подталкивает разработчиков к использованию многословных либо плохих решений наподобие
/* NullReferenceException instead of InvalidCastException */ public void EventHandler(object sender, EventArgs args) => ((sender as Button).DataContext as IModel).Update(); /* miss of InvalidCastException */ public void EventHandler(object sender, EventArgs args) => ((sender as Button)?.DataContext as IModel)?.Update(); /* verbose */ public void EventHandler(object sender, EventArgs args) { var button = (Button) sender; var model = (IModel) button.DataContext; model.Update(); }
Тем не менее существует менее очевидное, но более элегантное решение проблемы путём правостороннего приведения типа
public void EventHandler(object sender, EventArgs args) => sender.To<Button>().DataContext.To<IModel>().Update(); public static T To<T>(this object o) => (T) o;
При дальнейшем обобщении подхода мы получаем следующий набор методов-расширений
public static object ChangeType(this object o, Type type) => o == null || type.IsValueType || o is IConvertible ? Convert.ChangeType(o, type, null) : o; public static T To<T>(this T o) => o; public static T To<T>(this T o, out T x) => x = o; public static T To<T>(this object o) => (T) ChangeType(o, typeof(T)); public static T To<T>(this object o, out T x) => x = (T) ChangeType(o, typeof(T));
которые позволяют отзеркалить направление всех трёх базовых операций: декларации переменной, привидения типа и присваивания значения
sender.To(out Button b).DataContext.To(out IModel m).Update(); /* or */ sender.To(out Button _).DataContext.To(out IModel _).Update();
Эти примеры иллюстрируют, что исторически C# потерял что-то вроде оператора `to`. Сравните
((sender to Button b).DataContext to IModel m).Update(); ((sender to Button _).DataContext to IModel _).Update(); /* or even */ sender to Button b.DataContext to IModel m.Update(); sender to Button _.DataContext to IModel _.Update();
2. to-with паттерн
Многим разработчикам хорошо знакомы инициализационные блоки в духе `json`
var person = new Person { Name = "Abc", Age = 28, City = new City { Name = "Minsk" } };
вместо
var person = new Person(); person.Name = "Abc"; person.Age = 28; person.City = new City(); person.City.Name = "Minsk";
Они довольно хороши и удобны, поскольку позволяют структурировать код и сделать его более чистым и читаемым. Однако у таких блоков есть недостаток — они применимы лишь в связке с конструктором. Но иногда для создания объектов предпочтительно использование методов-фабрик, и, к сожалению, в таких случаях инициализационные блоки непригодны
var person = CreatePerson() { Name = "Abc", Age = 28, City { Name = "Minsk" } }; // cause compile errors
Другими словами, простая замена конструктора на метод-фабрику может вызывать кардинальную смену структуры кода. Как этого избежать?
Для начала рассмотрим два метода-расширения
public static T To<T>(this T o, out T x) => x = o; public static T With<T>(this T o, params object[] pattern) => o;
Они позволяют нам переписать код следующими способами
var person = new Person().To(out var p).With ( p.Name = "Abc", p.Age = 28, p.City = new City().To(out var c).With ( c.Name = "Minsk" ) );
либо
var person = CreatePerson().To(out var p)?.With ( p.Name = "Abc", p.Age = 28, p.City.To(out var c)?.With ( c.Name = "Minsk" ) );
* при желании можно поиграть с примерами в онлайн-компиляторе по ссылке
Это чуть более многословная, но обощённая запись, в сравнении с инициализационными блоками. Немаловажно, что поддерживаются рекурсивные выражения совместно с оператором проверки на `null` (`?`), а также вызовы функциональных методов, возвращающих значения, например,
var person = CreatePerson().To(out var p)?.With ( ... p.ToString().To(out var personStringView) );
Однако предложенная реализация метода `With` имеет несколько недостатков:
- создание массивов и выделение для них памяти (array allocations)
- возможная упаковка для типов-значений (boxing for value types)
Эти проблемы могут быть устранены следующим образом
public static T With<T>(this T o) => o; public static T With<T, A>(this T o, A a) => o; public static T With<T, A, B>(this T o, A a, B b) => o; public static T With<T, A, B, C>(this T o, A a, B b, C c) => o; /* ... */
Если же необходимо получить крупное, но хорошо оптимизированное `With` выражение, то допустима конкатенация (склеивание) нескольких более коротких выражений
GetModel().To(out var m) .With(m.A0 = a0, ... , m.AN = an).With(m.B0 = b0, ... ,m.BM = bM).Save();
Данный подход имеет производительность предельно близкую к идеальной.
Существует также неочевидный эффект, связанный со структурами. Для примера, если мы хотим модифицировать структуру и вернуть её в цепочку вызовов методов, то нам необходимо использовать `put`-паттерн
public static TX Put<T, TX>(this T o, TX x) => x; public static TX Put<T, TX>(this T o, ref TX x) => x;
Дело в том, что при вызове метода-расширения для структуры происходит её копирование, в результате чего метод `With` возвращает её оригинал вместо модифицированного экземпляра
static AnyStruct SetDefaults(this AnyStruct s) => s.With(s.Name = "DefaultName").Put(ref s);
Также `With` метод полезен в подобных сценариях
// possible NRE void UpdateAppTitle() => Application.Current.MainWindow.Title = title; // currently not supported by C#, possible, will be added later void UpdateAppTitle() => Application.Current.MainWindow?.Title = title; // classical solution void UpdateAppTitle() { var window = Application.Current.MainWindow; if (window != null) window.Title = title; } void UpdateAppTitle() => Application.Current.MainWindow.To(out var w)?.With(w.Title = title);
Это базовая информация о `to-with` паттерне, но не вся.
Неочевидная, но очень важная вещь — паттерн двунаправленный и зеркально симметричный относительно присваивания.
Это означает, что мы можем его использовать для инициализации и деконструкции объектов одновременно!
GetPerson().To(out var p).With ( /* deconstruction-like variations */ p.Name.To(out var name), /* right side assignment to the new variable */ p.Name.To(out nameLocal), /* right side assignment to the declared variable */ NameField = p.Name, /* left side assignment to the declared variable */ NameProperty = p.Name, /* left side assignment to the property */ /* a classical initialization-like variation */ p.Name = "AnyName" )
Как видно, обычные `json` подобные инициализационные блоки являются лишь ограниченной (отчасти из-за левостороннего присваивания) частной синтаксической вариацией намного более обобщённого `with` паттерна.
Кроме того, подобный подход применим и для инициализаторов коллекций
public CustomCollection GetSampleCollection() => new CustomCollection().To(out var c).With(c.Name = "Sample").Merge(a, b, c, d); /* currently not possible */ public CustomCollection GetSampleCollection() => new CustomCollection { Name = "Sample" } { a, b, c, d };
где
public static TCollection Merge<TCollection, TElement>( this TCollection collection, params TElement[] items) where TCollection : ICollection<TElement> => items.ForEach(collection.Add).Put(collection);
Возможно также реализовать очень близкий по духу `check` паттерн для условных выражений
if (GetPerson() is Person p && p.Check ( p.FirstName is "Keanu", p.LastName is string lastName, p.Age.To(out var age) > 23 ).All(true)) ... if (GetPerson() is Person p && p.Check ( p.FirstName.Is("Keanu"), /* check for equality */ p.LastName.Is(out var lastName), /* check for null */ p.City.To(out var city).Put(true), /* always true */ p.Age.To(out var age) > 23 ).All(true)) ... case Person p when p.Check ( p.FirstName.StartWith("K"), p.LastName.StartWith("R"), p.Age.To(out var age) > 23 ).Any(true): ... case Point p when p.Check ( p.X > 9, p.Y > 7 && p.Y < 221 p.Z > p.Y p.T > 0 ).Count(false) == 2: ...
Взгляните
public static bool[] Check<T>(this T o, params bool[] pattern) => pattern;
3. Другие фишки
put паттерн
Предназначен для смены контекста в цепочке вызовов, а также декларации произвольного значения в любом контексте
use паттерн
Позволяет объявить новую переменную в цепочке вызовов либо выполнить сторонний метод
if (GetPerson() is Person p && p.Check ( ... p.City.To(out var city).Put(true), /* always true */ p.Age.To(out var age) > 23 ).All(true)) ...
persons.Use(out var j, 3).ForEach(p => p.FirstName = $"Name{j++}");
private static bool TestPutUseChain() => int.TryParse("123", out var i).Put(i).Use(Console.WriteLine) == 123;
new паттерн
Предоставляет возможность использовать вывод типов при декларации массивов и коллекций, а также создавать объекты с помощью обобщённого метода
var words = New.Array("hello", "wonderful", "world"); var ints = New.List(1, 2, 3, 4, 5); var item = New.Object<T>();
value propagation / group assignment
Добавляет поддержку групповой инициализации переменных одним значением, в том числе в процессе декларации с выводом типа
var (x, y, z) = 0; (x, y, z) = 1; var ((x, y, z), t, n) = (1, 5, "xyz");
lambda-styled type matching
Альтернатива классическому оператору `switch` на основе лямбда-выражений
public static double CalculateSquare(this Shape shape) => shape.Match ( (Line _) => 0, (Circle c) => Math.PI * c.Radius * c.Radius, (Rectangle r) => r.Width * r.Height, () => double.NaN );
Детальные реализации и примеры кода находятся по ссылкам
Github mirror: implementation / some tests
Bitbucket mirror: implementation / some tests
Результаты
Рассмотренные расширения очень помогают при написании `expression-bodied`методов, а также позволяют сделать код более чистым и выразительным. И если ты тоже ощутил вкус этих расширений, то приятного применения на практике!
Послесловие от автора
Подготовка материалов для данной статьи потребовала очень значительных вложений свободного времени и сил, поэтому если ты сочтёшь информацию практически полезной, то можешь выразить свою благодарность в виде добровольного пожертвования произвольного размера или просто поделиться информацией с друзьями и коллегами. Это очень ценно!
