Деконструкция кортежей и других типов
Кортеж позволяет вам легко получить несколько значений при вызове метода. Но после получения кортежа вам нужно будет обработать его отдельные элементы. Работа с элементом по элементам является громоздкой, как показано в следующем примере. Метод QueryCityData
возвращает три кортежа, и каждый из его элементов назначается переменной в отдельной операции.
public class Example
{
public static void Main()
{
var result = QueryCityData("New York City");
var city = result.Item1;
var pop = result.Item2;
var size = result.Item3;
// Do something with the data.
}
private static (string, int, double) QueryCityData(string name)
{
if (name == "New York City")
return (name, 8175133, 468.48);
return ("", 0, 0);
}
}
Получение нескольких значений полей и свойств из объекта может быть столь же трудоемким: необходимо назначить переменной значение поля или свойства на основе члена.
Можно извлечь несколько элементов из кортежа или получить несколько полей, свойств и вычисляемых значений из объекта в рамках одной операции деконструкции . Чтобы деконструировать кортеж, необходимо назначить его элементы отдельным переменным. При деконструкции объекта вы присваиваете отдельным переменным выбранные значения.
Кортежи
Язык C# имеет встроенную поддержку деконструкции кортежей, которая позволяет извлекать из кортежа все элементы за одну операцию. Общий синтаксис деконструкции кортежа напоминает синтаксис его определения: переменные, которым будут присвоены элементы кортежа, указываются в круглых скобках в левой части оператора присваивания. Например, следующая инструкция назначает элементы четырех кортежей четырем отдельным переменным:
var (name, address, city, zip) = contact.GetAddressInfo();
Существует три способа деконструкции кортежа:
Вы можете явно объявить тип каждого поля в скобках. В следующем примере этот подход используется для деконструкции трех кортежей, возвращаемых методом
QueryCityData
.public static void Main() { (string city, int population, double area) = QueryCityData("New York City"); // Do something with the data. }
Вы можете использовать ключевое слово
var
, чтобы C# определил тип каждой переменной. Ключевое словоvar
помещается за пределами скобок. В следующем примере при деконструкции трех кортежей, возвращаемых методомQueryCityData
, используется определение типа.public static void Main() { var (city, population, area) = QueryCityData("New York City"); // Do something with the data. }
Кроме того, вы можете использовать ключевое слово
var
при объявлении отдельных или всех переменных внутри скобок.public static void Main() { (string city, var population, var area) = QueryCityData("New York City"); // Do something with the data. }
Это громоздкий и не рекомендуется.
Наконец, можно выполнить деконструкцию кортежа в переменные, которые уже были объявлены.
public static void Main() { string city = "Raleigh"; int population = 458880; double area = 144.8; (city, population, area) = QueryCityData("New York City"); // Do something with the data. }
Начиная с C# 10, можно смешивать объявление переменных и назначение в деконструкции.
public static void Main() { string city = "Raleigh"; int population = 458880; (city, population, double area) = QueryCityData("New York City"); // Do something with the data. }
Нельзя указать определенный тип вне круглых скобок, даже если каждое поле кортежа имеет один и тот же тип. При этом возникает ошибка компилятора CS8136, форма deconstruction "var (...)" запрещает определенный тип для "var".
Необходимо назначить каждому элементу кортежа переменной. Если опустить какие-либо элементы, компилятор создает ошибку CS8132: "Не удается деконструировать кортеж элементов x в переменные y".
Элементы кортежа с отменами
При деконструкции кортежа нас часто интересуют значения только некоторых элементов. Вы можете воспользоваться преимуществами поддержки C# для отклонений, которые являются переменными только для записи, значения которых вы решили игнорировать. Отмена выбирается символом подчеркивания ("_") в назначении. Вы можете сделать пустыми сколько угодно значений. Все они будут считаться одной переменной, _
.
В следующем примере показано использование кортежей с пустыми переменными. Метод QueryCityDataForYears
возвращает шесть кортежей с именем города, его области, годом, населением города за этот год, вторым годом и населением города на этот второй год. В примере показано изменение численности населения за эти два года. Из доступных в кортеже данных нас не интересует площадь города, а название города и две даты известны нам уже на этапе разработки. Следовательно, нас интересуют только два значения численности населения, которые хранятся в кортеже. Остальные значения можно обработать как пустые переменные.
using System;
public class ExampleDiscard
{
public static void Main()
{
var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010);
Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}");
}
private static (string, double, int, int, int, int) QueryCityDataForYears(string name, int year1, int year2)
{
int population1 = 0, population2 = 0;
double area = 0;
if (name == "New York City")
{
area = 468.48;
if (year1 == 1960)
{
population1 = 7781984;
}
if (year2 == 2010)
{
population2 = 8175133;
}
return (name, area, year1, population1, year2, population2);
}
return ("", 0, 0, 0, 0, 0);
}
}
// The example displays the following output:
// Population change, 1960 to 2010: 393,149
Определяемые пользователем типы
C# не поддерживает встроенную поддержку деконструкции типов не кортежей, отличных от record
типов DictionaryEntry . Тем не менее, если вы являетесь создателем класса, структуры или интерфейса, вы можете разрешить деконструкцию экземпляров определенного типа, реализовав один или несколько методов Deconstruct
. Метод возвращает "void", и каждое деконструируемое значение обозначается параметром out в сигнатуре метода. Например, следующий метод Deconstruct
класса Person
возвращает имя, отчество и фамилию:
public void Deconstruct(out string fname, out string mname, out string lname)
Затем можно деконструировать экземпляр Person
класса с именем p
назначения, как показано в следующем коде:
var (fName, mName, lName) = p;
В следующем примере показана перегрузка метода Deconstruct
для возвращения различных сочетаний свойств объекта Person
. Отдельные перегрузки возвращают следующие значения:
- Имя и фамилия.
- Имя, отчество, фамилия.
- Имя, фамилия, название города и название штата.
using System;
public class Person
{
public string FirstName { get; set; }
public string MiddleName { get; set; }
public string LastName { get; set; }
public string City { get; set; }
public string State { get; set; }
public Person(string fname, string mname, string lname,
string cityName, string stateName)
{
FirstName = fname;
MiddleName = mname;
LastName = lname;
City = cityName;
State = stateName;
}
// Return the first and last name.
public void Deconstruct(out string fname, out string lname)
{
fname = FirstName;
lname = LastName;
}
public void Deconstruct(out string fname, out string mname, out string lname)
{
fname = FirstName;
mname = MiddleName;
lname = LastName;
}
public void Deconstruct(out string fname, out string lname,
out string city, out string state)
{
fname = FirstName;
lname = LastName;
city = City;
state = State;
}
}
public class ExampleClassDeconstruction
{
public static void Main()
{
var p = new Person("John", "Quincy", "Adams", "Boston", "MA");
// Deconstruct the person object.
var (fName, lName, city, state) = p;
Console.WriteLine($"Hello {fName} {lName} of {city}, {state}!");
}
}
// The example displays the following output:
// Hello John Adams of Boston, MA!
Несколько методов Deconstruct
с одинаковым числом параметров вносят путаницу. Старайтесь определять методы Deconstruct
с разным числом параметров или аргументов. Методы Deconstruct
с одинаковым количеством параметров невозможно различить при разрешении перегрузки.
Определяемый пользователем тип с отменами
Как и с кортежами, пустые переменные можно применять с пользовательскими типами, чтобы игнорировать определенные элементы, возвращаемые методом Deconstruct
. Каждая пустая переменная определяется переменной с именем "_", и одна операция деконструкции может включать несколько пустых переменных.
В следующем примере показана деконструкция объекта Person
на четыре строки (имя, фамилия, город и область), но для фамилии и области используются пустые переменные.
// Deconstruct the person object.
var (fName, _, city, _) = p;
Console.WriteLine($"Hello {fName} of {city}!");
// The example displays the following output:
// Hello John of Boston!
Методы расширения для определяемых пользователем типов
Если вы не создали класс, структуру или интерфейс, можно по-прежнему деконструировать объекты этого типа, реализовав один или несколько Deconstruct
методов расширения , чтобы вернуть интересующие вас значения.
В приведенном ниже примере определены два метода расширения Deconstruct
для класса System.Reflection.PropertyInfo. Первый метод возвращает набор значений, которые указывают характеристики свойства, в том числе его тип, является ли оно статическим свойством или экземпляром, доступно ли оно только для чтения и является ли оно индексируемым. Второй метод показывает уровень доступа свойства. Так как методы доступа для чтения и записи у свойства могут иметь разный уровень доступа, мы используем логические значения, которые показывают, имеет ли свойство разные методы для чтения и записи и, если это так, имеют ли эти методы один уровень доступа. Если имеется только один метод доступа или как метод получения, так и метод доступа set имеют одинаковые специальные возможности, access
переменная указывает на доступность свойства в целом. В противном случае доступность методов чтения и записи указывается переменными getAccess
и setAccess
.
using System;
using System.Collections.Generic;
using System.Reflection;
public static class ReflectionExtensions
{
public static void Deconstruct(this PropertyInfo p, out bool isStatic,
out bool isReadOnly, out bool isIndexed,
out Type propertyType)
{
var getter = p.GetMethod;
// Is the property read-only?
isReadOnly = ! p.CanWrite;
// Is the property instance or static?
isStatic = getter.IsStatic;
// Is the property indexed?
isIndexed = p.GetIndexParameters().Length > 0;
// Get the property type.
propertyType = p.PropertyType;
}
public static void Deconstruct(this PropertyInfo p, out bool hasGetAndSet,
out bool sameAccess, out string access,
out string getAccess, out string setAccess)
{
hasGetAndSet = sameAccess = false;
string getAccessTemp = null;
string setAccessTemp = null;
MethodInfo getter = null;
if (p.CanRead)
getter = p.GetMethod;
MethodInfo setter = null;
if (p.CanWrite)
setter = p.SetMethod;
if (setter != null && getter != null)
hasGetAndSet = true;
if (getter != null)
{
if (getter.IsPublic)
getAccessTemp = "public";
else if (getter.IsPrivate)
getAccessTemp = "private";
else if (getter.IsAssembly)
getAccessTemp = "internal";
else if (getter.IsFamily)
getAccessTemp = "protected";
else if (getter.IsFamilyOrAssembly)
getAccessTemp = "protected internal";
}
if (setter != null)
{
if (setter.IsPublic)
setAccessTemp = "public";
else if (setter.IsPrivate)
setAccessTemp = "private";
else if (setter.IsAssembly)
setAccessTemp = "internal";
else if (setter.IsFamily)
setAccessTemp = "protected";
else if (setter.IsFamilyOrAssembly)
setAccessTemp = "protected internal";
}
// Are the accessibility of the getter and setter the same?
if (setAccessTemp == getAccessTemp)
{
sameAccess = true;
access = getAccessTemp;
getAccess = setAccess = String.Empty;
}
else
{
access = null;
getAccess = getAccessTemp;
setAccess = setAccessTemp;
}
}
}
public class ExampleExtension
{
public static void Main()
{
Type dateType = typeof(DateTime);
PropertyInfo prop = dateType.GetProperty("Now");
var (isStatic, isRO, isIndexed, propType) = prop;
Console.WriteLine($"\nThe {dateType.FullName}.{prop.Name} property:");
Console.WriteLine($" PropertyType: {propType.Name}");
Console.WriteLine($" Static: {isStatic}");
Console.WriteLine($" Read-only: {isRO}");
Console.WriteLine($" Indexed: {isIndexed}");
Type listType = typeof(List<>);
prop = listType.GetProperty("Item",
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
var (hasGetAndSet, sameAccess, accessibility, getAccessibility, setAccessibility) = prop;
Console.Write($"\nAccessibility of the {listType.FullName}.{prop.Name} property: ");
if (!hasGetAndSet | sameAccess)
{
Console.WriteLine(accessibility);
}
else
{
Console.WriteLine($"\n The get accessor: {getAccessibility}");
Console.WriteLine($" The set accessor: {setAccessibility}");
}
}
}
// The example displays the following output:
// The System.DateTime.Now property:
// PropertyType: DateTime
// Static: True
// Read-only: True
// Indexed: False
//
// Accessibility of the System.Collections.Generic.List`1.Item property: public
Метод расширения для системных типов
Некоторые системные типы предоставляют Deconstruct
метод в качестве удобства. Например, System.Collections.Generic.KeyValuePair<TKey,TValue> тип предоставляет эту функцию. При итерации по каждому System.Collections.Generic.Dictionary<TKey,TValue> элементу является и KeyValuePair<TKey, TValue>
может быть деконструированы. Рассмотрим следующий пример.
Dictionary<string, int> snapshotCommitMap = new(StringComparer.OrdinalIgnoreCase)
{
["https://github.com/dotnet/docs"] = 16_465,
["https://github.com/dotnet/runtime"] = 114_223,
["https://github.com/dotnet/installer"] = 22_436,
["https://github.com/dotnet/roslyn"] = 79_484,
["https://github.com/dotnet/aspnetcore"] = 48_386
};
foreach (var (repo, commitCount) in snapshotCommitMap)
{
Console.WriteLine(
$"The {repo} repository had {commitCount:N0} commits as of November 10th, 2021.");
}
Вы можете добавить метод в системные Deconstruct
типы, у которых его нет. Рассмотрим следующий метод расширения:
public static class NullableExtensions
{
public static void Deconstruct<T>(
this T? nullable,
out bool hasValue,
out T value) where T : struct
{
hasValue = nullable.HasValue;
value = nullable.GetValueOrDefault();
}
}
Этот метод расширения позволяет деконструировать все Nullable<T> типы в кортеж (bool hasValue, T value)
. В следующем примере показан код, использующий этот метод расширения:
DateTime? questionableDateTime = default;
var (hasValue, value) = questionableDateTime;
Console.WriteLine(
$"{{ HasValue = {hasValue}, Value = {value} }}");
questionableDateTime = DateTime.Now;
(hasValue, value) = questionableDateTime;
Console.WriteLine(
$"{{ HasValue = {hasValue}, Value = {value} }}");
// Example outputs:
// { HasValue = False, Value = 1/1/0001 12:00:00 AM }
// { HasValue = True, Value = 11/10/2021 6:11:45 PM }
record
Типы
При объявлении типа record с помощью двух позиционных параметров или более компилятор создает метод Deconstruct
с параметром out
для каждого позиционного параметра в объявлении record
. Дополнительные сведения см. в разделах Позиционный синтаксис для определения свойства и Поведение деконструктора в производных записях.