- PVSM.RU - https://www.pvsm.ru -
Типы System.Tuple были введены в .NET 4.0 с двумя существенными недостатками:
Чтобы решить эти проблемы, в C# 7 представлена новая возможность языка, а также новое семейство типов (*).
Сегодня, если вам нужно склеить два значения, чтобы вернуть их из функции или поместить два значения в хэш-набор, вы можете использовать типы System.ValueTuple и создать их с помощью удобного синтаксиса:
// Constructing the tuple instance
var tpl = (1, 2);
// Using tuples with a dictionary
var d = new Dictionary<(int x, int y), (byte a, short b)>();
// Tuples with different names are compatible
d.Add(tpl, (a: 3, b: 4));
// Tuples have value semantic
if (d.TryGetValue((1, 2), out var r))
{
// Deconstructing the tuple ignoring the first element
var (_, b) = r;
// Using named syntax as well as predefined name
Console.WriteLine($"a: {r.a}, b: {r.Item2}");
}
(*) Типы System.ValueTuple представлены в .NET Framework 4.7. Но вы можете использовать их в более ранних версиях фреймворка, в этом случае вам нужно добавить в проект специальный пакету nuget: System.ValueTuple [1].
(**) Мы скоро увидим, что это не всегда так.
Отсутствие пользовательских имен делает типы System.Tuple не очень полезными. Я могу использовать System.Tuple как часть реализации небольшого метода, но если мне нужно передать его экземпляр, я предпочитаю именованный тип с описательными именами свойств. Кортежи в C# 7 довольно элегантно решают эту проблему: вы можете указать имена для элементов кортежа и, в отличие от анонимных классов, эти имена доступны даже в разных сборок.
Компилятор C# генерирует специальный атрибут TupleElementNamesAttribute (***) для каждого типа кортежа, используемого в сигнатуре метода:
(***) Атрибут TupleElementNamesAttribute является специальным и не может использоваться непосредственно в коде пользователя. Компилятор выдает ошибку, если вы попытаетесь его использовать.
public (int a, int b) Foo1((int c, int d) a) => a;
[return: TupleElementNames(new[] { "a", "b" })]
public ValueTuple<int, int> Foo(
[TupleElementNames(new[] { "c", "d" })] ValueTuple<int, int> a)
{
return a;
}
Данный атрибут помогает IDE и компилятору «видеть» имена элементов и предупреждать, если они используются неправильно:
// Ok: tuple literal can skip element names
(int x, int y) tpl = (1, 2);
// Warning: The tuple element 'a' is ignored because a different name
// or no name is specified by the target type '(int x, int y)'.
tpl = (a:1, b:2);
// Ok: tuple deconstruction ignore element names
var (a, b) = tpl;
// x: 2, y: 1. Tuple names are ignored
var (y, x) = tpl;
У компилятора более высокие требования к унаследованным членам:
public abstract class Base
{
public abstract (int a, int b) Foo();
public abstract (int, int) Bar();
}
public class Derived : Base
{
// Error: Cannot change tuple element names when overriding method
public override (int c, int d) Foo() => (1, 2);
// Error: Cannot change tuple element names when overriding method
public override (int a, int b) Bar() => (1, 2);
}
Обычные аргументы метода могут быть свободно изменены в переопределенных членах, но имена элементов кортежей в переопределенных членах должны точно совпадать с именами из базового типа.
C # 7.1 появилось одно дополнительное усовершенствование: вывод имени элемента кортежа аналогичен тому, что C# делает для анонимных типов.
public void NameInference(int x, int y)
{
// (int x, int y)
var tpl = (x, y);
var a = new {X = x, Y = y};
// (int X, int Y)
var tpl2 = (a.X, a.Y);
}
Кортежи являются изменяемыми значимыми типами. Мы знаем, что изменяемые значимые типы считаются вредными. Вот небольшой пример их злой природы:
var x = new { Items = new List<int> { 1, 2, 3 }.GetEnumerator() };
while (x.Items.MoveNext())
{
Console.WriteLine(x.Items.Current);
}
Если вы запустите этот код, вы получите… бесконечный цикл. Список List .Enumerator — это изменяемый значимый типа, а Items свойство. Это означает, что x.Items возвращает копию исходного итератора на каждой итерации цикла, вызывая бесконечный цикл.
Но изменяемые значимые типы опасны только тогда, когда данные смешиваются с поведением: Enumerator содержит состояние (текущий элемент) и имеет поведение (возможность продвижения итератора путем вызова метода MoveNext). Эта комбинация может вызывать проблемы, потому что легко вызвать метод на копии, вместо исходного экземпляра, что приводит к эффекту no-op (No Operation). Вот набор примеров, которые могут вызвать неочевидное поведение из-за скрытой копии типа значения: gist [3].
Кортежи обладают состоянием, но не поведением, поэтому приведенные выше проблемы к ним не применимы. Но одна проблема с изменчивостью все же остается:
var tpl = (x: 1, y: 2);
var hs = new HashSet<(int x, int y)>();
hs.Add(tpl);
tpl.x++;
Console.WriteLine(hs.Contains(tpl)); // false
Кортежи являются очень полезными в качестве ключей в словарях и могут использоваться в качестве ключей благодаря семантики значений. Но не следует изменять состояние переменной ключа между различными операциями с коллекцией.
Несмотря на то, что язык C# обладает специальным синтаксисом для создания экземпляров кортежей, деконструкция является более общей возможностью и может использоваться с любым типом.
public static class VersionDeconstrucion
{
public static void Deconstruct(this Version v, out int major, out int minor, out int build, out int revision)
{
major = v.Major;
minor = v.Minor;
build = v.Build;
revision = v.Revision;
}
}
var version = Version.Parse("1.2.3.4");
var (major, minor, build, _) = version;
// Prints: 1.2.3
Console.WriteLine($"{major}.{minor}.{build}");
Разбор (деконструкция) кортежа использует подход «утиной типизации»: если компилятор может найти метод Deconstruct для данного типа – экземплярный метод или метод расширения — тип является разбираемым.
После того, как вы начнете использовать кортежи, вы быстро поймете, что хотите «повторно использовать» тип кортежа с именованными элементами в нескольких местах исходного кода. Но с этим не все так просто.
Во-первых, C # не поддерживает глобальные псевдонимы для заданного типа. Вы можете использовать 'using' alias директиву, но она создает псевдоним, видимый в одном файле.
Во-вторых, вы даже не можете использовать эту возможность совместно с кортежами:
// You can't do this: compilation error
using Point = (int x, int y);
// But you *can* do this
using SetOfPoints = System.Collections.Generic.HashSet<(int x, int y)>;
Сейчас на github в теме «Типы Tuple при использовании директив» [4] идет обсуждение этой проблемы. Поэтому, если вы обнаружите, что используете один тип кортежа в нескольких местах, у вас есть два варианта: либо копировать во типы по всей кодовой базе либо создать именованный тип.
Pascal case, например ElementName, или camel case, например elementName? С одной стороны, элементы кортежей должны следовать правилу именования для публичных членов (т.е. PascalCase), но, с другой стороны, кортежи — это просто хранилище для переменных, а переменные именуются с camelСase.
Вы можете использовать следующий подход:
Но я предпочитаю использовать camelCase все время.
Я нашел кортежи очень полезными в повседневной работе. Мне нужно больше одного возвращаемого значения из функции, или мне нужно поместить пару значений в хэш-набор, или мне нужно изменить словарь и сохранить не одно значение, а два, или ключ становится более сложным, и мне нужно расширить его другим полем.
Я даже использую их, чтобы избежать аллокации замыкания с помощью таких методов, как ConcurrentDictionary.TryGetOrAdd, который теперь принимает дополнительный аргумент. И во многих случаях, состояние также является кортежем.
Эти фичи очень полезны, но я действительно хочу увидеть несколько улучшений:
(****) Я знаю, что эта функция спорная, но я думаю, что это будет очень полезно. Мы можем дождаться типов Record, но я не уверен, будут ли записи значимыми типами или ссылочными типами.
Автор: Дзеранов Иосиф
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/271475
Ссылки в тексте:
[1] System.ValueTuple: https://www.nuget.org/packages/System.ValueTuple/
[2] «Поддержка == и! = Для типов кортежей»: https://github.com/dotnet/csharplang/issues/190
[3] gist: https://gist.github.com/SergeyTeplyakov/8841519120c9858324314e25ddccfc52
[4] «Типы Tuple при использовании директив»: https://github.com/dotnet/csharplang/issues/423
[5] Источник: https://habrahabr.ru/post/345376/?utm_source=habrahabr&utm_medium=rss&utm_campaign=345376
Нажмите здесь для печати.