Паттерн свойств


Паттерн свойств позволяет сравнивать со значениями определенных свойств объекта. Например, пусть у нас будет следующий класс:

class Person
{
public string Name { get; set; } = ""; // имя пользователя
public string Status { get; set; } = ""; // статус пользователя
public string Language { get; set; } = ""; // язык пользователя
}
Например, в зависимости от языка пользователя выведем ему определенное сообщение, применив паттерн свойств:

Person tom = new Person { Language = "english", Status = "user", Name = "Tom" };
Person pierre = new Person { Language = "french", Status = "user", Name = "Pierre" };

SayHello(tom); // Hello
SayHello(pierre); // Salut

void SayHello(Person person)
{
if(person is Person { Language: "french" })
{
Console.WriteLine("Salut");
}
else
{
Console.WriteLine("Hello");
}
}
Здесь метод SayHello в качестве параметра принимает объект Person и сопоставляет его с некоторым паттерном. В качестве паттерна выступает выражение Person { Language: "french" }. То есть параметр person должен представлять объект Person, у которого значение свойства Language равно "french".

При этом можно задействовать набор свойств. Например, добавим проверку по свойству Status:

Person tom = new Person { Language = "english", Status = "user", Name = "Tom" };
Person pierre = new Person { Language = "french", Status = "user", Name = "Pierre" };
Person admin = new Person { Language = "english", Status = "admin", Name = "Admin" };

SayHello(admin); // Hello, admin
SayHello(tom); // Hello
SayHello(pierre); // Salut

void SayHello(Person person)
{
if(person is Person { Language: "english", Status: "admin" })
{
Console.WriteLine("Hello, admin");
}
else if (person is Person { Language: "french"})
{
Console.WriteLine("Salut");
}
else
{
Console.WriteLine("Hello");
}
}
Теперь выражение if проверяет, соответствует ли параметр person объекту Person, у которого свойства Language и Status имеют определенные значения.

Подобным образом можно применять паттерн свойств в конструкции switch:

string GetMessage(Person? p) => p switch
{
{ Language: "english" } => "Hello!",
{ Language: "german", Status: "admin" } => "Hallo, admin!",
{ Language: "french" } => "Salut!",
{ } => "undefined",
null => "null" // если Person p = null
};
Паттерны свойств предполагают использование фигурных скобок, внутри которых указываются свойства и через двоеточие их значение {свойство: значение}. И со значением свойства в фигурных скобках сравнивается свойство передаваемого объекта. При этом в фигурных скобках мы можем указать несколько свойств и их значений { Language: "german", Status: "admin" } - тогда свойства передаваемого объекта должны соответствовать всем этим значениям.

Можно оставить пустые фигурные скобки, как в последнем случае { } => "undefined!" - передаваемый объект будет соответствовать пустым фигурным скобкам, если он не соответствует всем предыдущим значениям, или например, если его свойства не указаны или имеют значение null.

То есть в данном случае, если у объекта Person p выполняется равенство Language = "english", будет возвращаться строка "Hello!".

Если у объекта Person p одновременно выполняются два равенства Language = "german" и Role="admin", будет возвращаться строка "Hallo, admin!".

Если у объекта Person p выполняется равенство Language = "french", будет возвращаться строка "Salut!".

Если объект Person будет сопоставляться с пустыми фигурными скобками {}, и будет возвращаться строка "undefined".

Последняя проверка проверяет значение на null.

Применение:

Person pierre = new Person { Language = "french", Status = "user", Name = "Pierre" };
string message = GetMessage(pierre);
Console.WriteLine(message); // Salut!

Person tomas = new Person { Language = "german", Status = "admin", Name = "Tomas" };
Console.WriteLine(GetMessage(tomas)); // Hallo, admin!

Person pablo = new Person { Language = "spanish", Status = "user", Name = "Pablo" };
Console.WriteLine(GetMessage(pablo)); // undefined

Console.WriteLine(GetMessage(null)); // null
Кроме того, мы можем определять в паттерных свойств переменные, передавать этим переменным значения объекта и использовать при возвращении значения:

string GetMessage(Person? p) => p switch
{
{ Language: "german", Status: "admin" } => "Hallo, admin!",
{ Language: "french", Name: var name } => $"Salut, {name}!",
{ Language: var lang} => $"Unknown language: {lang}",
null => "null"
};
Так, подвыражение Name: var name говорит, что надо передать в переменную name значение свойства Name. Затем ее можно применить при генерации выходного значения: => $"Salut, {name}!"

Применение:

Person pierre = new Person { Language = "french", Status = "user", Name = "Pierre" };
string message = GetMessage(pierre);
Console.WriteLine(message); // Salut, Pierre!

Person tomas = new Person { Language = "german", Status = "admin", Name = "Tomas" };
Console.WriteLine(GetMessage(tomas)); // Hallo, admin!

Person pablo = new Person { Language = "spanish", Status = "user", Name = "Pablo" };
Console.WriteLine(GetMessage(pablo)); // Unknown language: spanish

Person? bob = null;
Console.WriteLine(GetMessage(bob)); // null
Стоит отметить, что начиная с версии C# 10 было упрощено сопоставление со свойствами вложенных объектов. Допустим, у нас есть следующие классы:

class Employee
{
public string Name { get;}
public Company Company { get; set; }
public Employee(string name, Company company)
{
Name = name;
Company = company;
}

}
class Company
{
public string Title { get;}
public Company(string title) => Title = title;
}
Класс Company определяет свойство Title, которое хранит название компании. Класс Employee определяет сотрудника компании и в свойстве Company хранит компанию. Применим паттерн свойств на основе свойств вложенного объекта Company:

var microsoft = new Company("Microsoft");
var google = new Company("Google");
var tom = new Employee("Tom", microsoft);
var bob = new Employee("Bob", google);

PrintCompany(tom); //
PrintCompany(bob); //

void PrintCompany(Employee employee)
{
if (employee is Employee { Company:{Title: "Microsoft" } })
{
Console.WriteLine($"{employee.Name} works in Microsoft");
}
else
{
Console.WriteLine($"{employee.Name} works someware");
}
}
В методе PrintCompany объект employee сопоставляется с паттерном Employee { Company:{Title: "Microsoft" } }. То есть сотрудник компании должен представлять объект Employee, у которого название компании равно "Microsoft"

Однако мы также можем сократить данный паттерн следующим образом:

void PrintCompany(Employee employee)
{
if (employee is Employee { Company.Title: "Microsoft" })
{
Console.WriteLine($"{employee.Name} works in Microsoft");
}
else
{
Console.WriteLine($"{employee.Name} works someware");
}
}