Сопоставление с Образцом и Компилятор Могут Удивлять
Сопоставление с образцом — мощная функция C#. Она позволяет сопоставлять значение с шаблоном и извлекать информацию из значения. Компилятор творит за вас волшебство, но иногда этим и поражает.
Вот такой фрагмент кода рассылки писем:
Console.WriteLine(
GetHashCode(new PostCard { Subject = "Test" }));
static int GetHashCode(Document doc)
{
return doc switch
{
PostCard { Subject: { } s, Text: { } t }
=> HashCode.Combine(s, t),
Mail { Subject: { } s }
=> s.GetHashCode(),
_ => 0,
};
}
public class Document;
public class Mail : Document
{
public string? Subject { get; set; }
}
public class PostCard : Mail
{
public string? Text { get; set; }
}
Функция GetHashCode принимает Document и возвращает хэш-код. Она использует сопоставление с образцом для сопоставления типа и получения хэш-кода. Интересная часть
PostCard { Subject: { } s, Text: { } t } по сути гарантирует, что Subject и Text не null. Второе условие проверяет только базовый тип (мы идём от более конкретного к более общему). В чём тут сюрприз? В том, что код выдаёт предупреждение:Program.cs(11,9): Warning CS8602 : Dereference of a possibly null reference (Разыменование возможно нулевой ссылки).
Вот в этом месте
Mail { Subject: { } s }
=> s.GetHashCode(),Учитывая, что мы явно проверяем, что Subject не null, почему компилятор на это жалуется? Для этого нам нужно взглянуть на сгенерированный компилятором код:
internal static GetHashCode(Document doc)
{
PostCard postCard = doc as PostCard;
string subject2;
if (postCard != null)
{
string subject = postCard.Subject;
if (subject != null)
{
string text = postCard.Text;
if (text == null)
{
Mail mail = (Mail)doc;
subject2 = mail.Subject;
goto IL_0057;
}
return HashCode.Combine(subject, text);
}
}
else
{
Mail mail = doc as Mail;
if (mail != null)
{
subject2 = mail.Subject;
if (subject2 != null)
{
goto IL_0057;
}
}
}
return 0;
IL_0057:
return subject2.GetHashCode();
}
Кажется, статический анализатор видит проблему в операторе goto в ветке PostCard. Заметьте, что subject проверяется на null, a subject2 нет.
Так что, если в вашем коде есть именно такой шаблон, возможно, вам стоит об этом знать. Оговорюсь, что случаи, когда этот код фактически выдаст исключение NullReferenceException – крайне редкие. Вот такой вымышленный пример:
public class Mail : Document
{
private string? subject;
public string? Subject
{
get
{
Console.WriteLine("Вызвали get");
var r = subject;
subject = null;
return r;
}
set => subject= value;
}
}
В этом случае при вызове
Console.WriteLine(
GetHashCode(new PostCard { Subject = "Test" }));
аксессор get свойства
Mail.Subject будет вызван дважды:1) в строке
string subject = postCard.Subject;2) в строке
subject2 = mail.Subject;И из-за нашего специального кода get выше, во втором случае get выдаст null, поэтому
return subject2.GetHashCode(); выбросит NullReferenceException. Выполнив этот код, вы увидите «Вызвали get» в консоли дважды перед исключением.Источник: https://steven-giesel.com/blogPost/9892bd03-96bf-4ab0-aacc-973a99d5f5e0/pattern-matching-and-the-compiler-can-be-surprising