Хочу рассказать вам историю о том, как синтаксический сахар может отнять у вас несколько часов и привести к ошибкам на продакшене. Так же разберу причины самих ошибок и постараюсь найти варианты, как можно было бы их избежать.
Сразу стоит отметить, что история, которую я опишу ниже, произошла с разработчиком из моей команды, а я лишь выступал в качестве наблюдателя.
Синтаксический сахар в C#
В нашей команде используется язык C#, поэтому мой рассказ будет про его синтаксис.
Синтаксический сахар заменяет длинные и часто используемые конструкции кода, делает их более короткими и быстро читаемыми, облегчает и ускоряет разработку.
Здесь стоит отметить и другое мнение: существуют люди, которые считают, что синтаксический сахар только усложняет код, делая его менее читаемым.
Я придерживаюсь данного мной определения, хотя опыт показал, что не всё так однозначно. Разберем на примерах:
Примеры:
Конструкция
new () {1, 2}
заменяетnew List() {1, 2}
Оператор
+=
заменяет конструкциюnumber += 1
=>number = number + 1
А оператор
??=
вообще имеет долгую историюfoo ??= "Строка была равна null"
=>foo = foo ?? "Строка была равна null"
=>foo = foo is null ? "Строка была равна null" : foo
=>if (foo is null)
{
foo = "Строка была равна null";
}
Эти примеры лишь малая часть всех тех конструкций, которые иcпользуются в C#. С каждой новой версией языка появляется всё больше новых конструкций и нужно пристально следить, чтобы ничего не упустить.
Неожиданные Null Reference Exception
Итак, перейдем к истории. Всё началось с того момента, когда в нашем проекте начали "выстреливать" ошибки NRE. По стек-трейсу было ясно, что код "падает" при инициализации класса. Ниже представлен фрагмент кода, приближенный к тому, что был у нас в проекте.
...
var result = new ExampleClass
{
ExString = "Тут не должно быть взрыва",
ExList = { 0, 1 },
ExString2 = "И тут",
ExInt = 24
};
...
NRE выбрасывалось на строке var result = new ExampleClass
, которая, в теории, не должна вызывать такую ошибку. Отсюда стало очевидно, что следует обратить внимание на то, как инициализируются поля объекта.
Вот как выглядел класс, который мы пытались инициализировать:
public class ExampleClass
{
public string? ExString { get; set; }
public List<int>? ExList { get; set; }
public string? ExString2 { get; set; }
public int ExInt { get; set; }
}
Наверное, я оставил очень прозрачные подсказки в коде, чтобы вы смогли догадаться, из-за чего выбрасывалась ошибка. Но в моменте было совсем не ясно, особенно когда ты пытаешься быстро разобраться в ситуации из-за ошибок на продакшене.
Пояснение
Можно обратить внимание на весьма странный синтаксис в коде.
...
var result = new ExampleClass
{
...
ExList = { 0, 1 },
...
};
...
Выше я приводил пример того, как конструкция new() {0, 1}
заменяет new List() {0, 1}
. Можно заметить, что {0, 1}
уж очень похоже на new() {0, 1}
.
Первый раз, глядя на эту конструкцию, в голову совсем не приходит мысль о том, что тут что-то не так. Скорее возникает мысль: "Наверное, это новый синтаксис". После 10 минут интенсивного просмотра кода было принято решение заменить {0, 1}
на new() {0, 1}
. И, о чудо, ошибки исчезли.
Но причём тут NRE? А всё дело в том, что{0, 1}
заменяет конструкцию .Add()
И вот эти два блока кода оказываются идентичными:
// 1
var result1 = new ExampleClass
{
ExList = { 1, 2 },
ExString = "Привет",
ExInt = 24
};
// 2
var result2 = new ExampleClass
{
ExString = "Привет",
ExInt = 24
};
result.ExList.Add(1);
result.ExList.Add(2);
Так как ExList
при инициализации равен null
, то при попытке вызывать null.Add()
мы получаем NRE.
Почему такое произошло?
Лично я вижу здесь несколько причин. Во-первых, в нашей команде и вокруг неё этот синтаксис не так популярен, с ним редко кто сталкивается. Во-вторых, IDE никак не подсвечивает такую простую NRE, хотя поле было помечено как nullable, и такое можно было бы подсветить.
Как этого избежать?
Самое главное — быть внимательным и проверять код. Можно попробовать следить за актуальными конструкциями, но, по моему опыту, это сделать весьма проблематично. Как ни странно, хоть IDE в этом случае подвело, но оно все равно умеет замечать большинство таких проблем. Именно поэтому стоит прислушиваться к его предупреждениям.
Заключение
В заключении хочу сказать, что несмотря на такие ситуации, синтаксический сахар остается мощным инструментом и приносит больше пользы, чем вреда. Чтобы избежать таких проблем, важно следить за глубоким пониманием того, как работает ваш код, а не только за тем, как он выглядит. Регулярный код-ревью и обмен опытом с коллегами помогут вам выявлять и предотвращать подобные ошибки заранее.
P.S.
Это одна из первых моих статей на Habr, и я был бы благодарен за вашу конструктивную критику и обратную связь.