C# 8: нет больше NullReferenceExceptions - Как насчет устаревшего кода?
В руководстве .NET указано, что приложение никогда не должно выдавать исключение NullReferenceException. Тем не менее, многие приложения и библиотеки делают это. Исключение NullReferenceException является наиболее распространенным исключением. Вот почему C# 8 пытается избавиться от него. С C# 8 ссылочные типы не являются нулевыми по умолчанию. Это большое изменение и отличная возможность. Однако, как насчет всего устаревшего кода? Могут ли старые библиотеки использоваться с приложениями C# 8, и могут ли приложения C# 7 использовать библиотеки C# 8?
В этой статье показано, как C# 8 позволяет смешивать старые и новые сборки.
Почему избегаются исключения NullReferenceException?
Когда возникает исключение NullReferenceException, найти причину часто бывает нелегко. Ошибки происходят в местах, находящихся далеко от реальной проблемы. Вот почему приложения не должны выдавать NullReferenceException, а вместо этого проверять наличие нулевых значений и выдавать ArgumentNullException. Если нулевое значение передается с аргументом, в элементе метода его можно проверить, чтобы не принять нулевое значение. Выдавая ArgumentNullException здесь, легко обнаружить, где проблема.
Давайте посмотрим, что делает C# 8, чтобы избежать исключения NullReferenceException.
Установка C# 8
На момент написания этой статьи C# 8 еще не выпущен. Однако вы можете попробовать его. На момент написания этой статьи, чтобы попробовать нулевые ссылочные типы, требуется Visual Studio 2017 15.5-15.7 - и предварительный просмотр C# Nullable Reference Types. Вы можете легко установить его и удалить.
При установке этой версии компилятора вы получите много предупреждений о многих ваших существующих проектах C#. По умолчанию используется последняя основная версия компилятора C#. Чтобы избавиться от предупреждений, вы можете указать точную версию компилятора C# на более раннюю версию с вашими существующими проектами или снова удалить компилятор C# 8.
Ссылочные типы не являются нулевыми
Новая допустимость неопределенного значения понятна. Синтаксис аналогичен типам значений NULL. Подобно типам значений, if the?, которые не указываются с описанием типа, null не допускается:
int i1 = 4; // null is not allowed int? i2 = null; // null is allowed string s1 = "a string"; // null is not allowed string? s2 = null; // a nullable string
Хотя синтаксис со значениями типов и ссылочными типами теперь выглядит схожим, функциональность сильно отличается.
- С типами значений компилятор C# использует тип Nullable. Этот тип является типом значения и добавляет логическое поле для определения, является ли тип значения нулевым или нет.
- С ссылочными типами компилятор C# добавляет атрибут Nullable. Версия 8 компилятора знает об этом атрибуте и ведет себя соответственно. C# 7 и более старые версии C# не знают об этом атрибуте и просто игнорируют его.
Компиляция программы с C# 8, как Book b, так и Book? b становится Book b с C# 7.
Следующий класс Book определяет не допускающие неопределенное значение свойства Title и Publisher, и нулевое свойство Isbn. В дополнение к этому, этот тип содержит конструктор, использующий кортежи C# 7 и деконструкцию. Используя тип книги и доступ к свойству Isbn, значение может быть записано только в переменную типа string ?. Присвоение ему строки приводит к предупреждению о компиляции C#, которое преобразует нулевой литерал или возможное значение null в тип, не допускающий неопределенное значение.
class Book { public string Title { get; } public string Publisher { get; } public string? Isbn { get; } public Book(string title, string publisher, string? isbn) => (Title, Publisher, Isbn) = (title, publisher, isbn); public Book(string title, string publisher) : this(title, publisher, null) { } public void Deconstruct(out string title, out string publisher, out string? isbn) => (title, publisher, isbn) = (Title, Publisher, Isbn); public override string ToString() => Title; }
var book = new Book("Professional C# 8", "Wrox Press"); // string isbn = book.Isbn; // error: converting null literal or possible null value to non-nullable type string? isbn = book.Isbn;
Присвоение Nullable к Non-nullable
Если вам нужно присвоить тип с нулевым значением (например, свойство Isbn из класса Book), C# 8 анализирует код. В фрагменте кода, поскольку isbn сравнивается с NULL, после оператора if isbn больше не может иметь значение null. После утверждения if нормально возвращать переменную isbn типа string? хотя метод объявлен для возврата строки, не допускающей неопределенное значение:
static string GetIsbn1(Book book) { string? isbn = book.Isbn; if (isbn == null) { return string.Empty; } return isbn; }
Конечно, вы также можете использовать коалесцирующий оператор. Это также позволяет использовать простую Лямбду с внедрением метода:
public string GetIsbn2(Book book) => book.Isbn ?? string.Empty;
Возвращение и переход к методам
Класс NewAndGlory определен в библиотеке классов, построенной с C# 8. Метод GetANullString определен для возврата строки типа ?, поэтому null разрешен, и этот метод просто возвращает null. Метод GetAString определен для возврата типа строки, поэтому null не допускается. С помощью метода PassAString параметр определяется для получения строки. Здесь null не допускается, и нет необходимости проверять это.
public class NewAndGlory { public string? GetANullString() => null; public string GetAString() => "a string"; public string PassAString(string s) => s.ToUpper(); }
С другой стороны, есть библиотека TheOldLib, которая использует компилятор C# 7.0. Это определено в файле проекта TheOldLib.csproj с элементом 7. Класс Legacy определяет метод GetANullString, который просто возвращает null, и метод PassAString, который получает строку и выполняет обычное тестирование для null до того, как используется строка. Эта библиотека также определяет интерфейс ILegacyInterface, который определяет метод, который возвращает строку. Эта строка может быть обозначена как nullable или нет, а C# 7 это не может быть указано в интерфейсе.
public class Legacy { public string GetANullString() => null; public string PassAString(string s) { if (s == null) throw new ArgumentNullException(nameof(s)); return s.ToUpper(); } } public interface ILegacyInterface { string Foo(); }
Приложение C# 8 с использованием библиотек, построенных с C# 7 и C# 8
Теперь давайте перейдем в приложение C# 8 Console, которое ссылается как на старые, так и на новые библиотеки. С помощью класса NewAndGlory, как предполагалось, результат метода GetNullString может быть записан только в тип string?. Попытка передать null методу PassAString в результате ошибки компиляции не может преобразовать нулевой литерал в ссылочный тип, не допускающий неопределенное значение, или параметр без ограничений.
Вызов класса Legacy, где метод GetANullString возвращает значение null, результат может быть записан в виде строки. Поскольку эта библиотека не реализована с C# 8, компилятор C# 8 не создает предупреждение. Это происходит только с новыми библиотеками. Также можно вызвать метод PassAString и передать значение null. Используя устаревшие библиотеки, будет показано слишком много ошибок, поэтому библиотеки, не построенные с новым компилятором, обрабатываются по-разному.
var newglory = new NewAndGlory(); string? s1 = newglory.GetANullString(); string s2 = newglory.GetAString(); // string s3 = newglory.PassAString(null); // error: cannot convert null literal to non-nullable reference or unconstrained type parameter var old = new Legacy(); string s4 = old.GetANullString(); // no error, s1 is null! string s5 = old.PassAString(null); // no error
Метод Foo интерфейса ILegacyInterface, определенный в библиотеке, построенной с помощью компилятора C# 7, возвращает строку. Как это можно реализовать с C# 8? Как вы можете видеть в следующем фрагменте кода, интерфейс может быть реализован в обоих направлениях - возврат строки с нулевым значением или строки с нулевым значением. Это хороший способ, так как с C# 7 невозможно было заявить о том, как это имелось в виду.
class SomeClass : ILegacyInterface { public string? Foo() => null; } class AnotherClass : ILegacyInterface { public string Foo() => "a string"; }
Интерфейсы, объявленные с помощью компилятора C# 8, нуждаются в правильной реализации в отношении допустимости определенного значения.
Приложение C# 7 с использованием библиотек, построенных на C# 8
Из старого приложения (приложение с использованием C# 7 или более раннего) новая C# 8 встроенная библиотека может использоваться как любая другая библиотека .NET. Новое приложение не видит типы с нулевым значением, такие как строка ?, и вместо этого видит строку, которая в любом случае является нулевой для C# 7. Необязательный тип строки становится типом нулевой строки. Таким образом, старое приложение может использовать новую библиотеку. Конечно, теперь в новой библиотеке есть те же проблемы в отношении допустимости, что и в старой библиотеке:
var glory = new NewAndGlory(); string s1 = glory.GetANullString(); string s2 = glory.GetAString(); string s3 = glory.PassAString(null); // having a NullReferenceException here!
Вызов метода PassAStringMethod и передачи null генерирует исключение NullReferenceException, когда метод ToUpper вызывается в строке. Это исключение, которого мы хотим избежать, но оно все еще встречается в таком сценарии взаимодействия. Чтобы этого избежать, в библиотеке C# 8 мы все еще можем проверить значение null и выбросить ArgumentNullException, которое не требуется для клиентов C# 8. Возможно, следующая версия компилятора C# 8 автоматически создает эту реализацию с типами non-nullable для более старых клиентов C#, но это необходимо только тогда, когда используются более старые компиляторы C# 8.
РЕЗЮМЕ
Ненулевые ссылочные типы - это новая функциональность C#, которая избавится от многих исключений NullReferenceException. Это стало возможным благодаря изменению поведения типов ссылок по умолчанию. Хотя поведение по умолчанию меняется, новое приложение C# 8 все еще может использовать старые библиотеки, а старое приложение C# может использовать новые библиотеки C# 8. Nullability реализуется с использованием атрибутов, что делает это возможным.