很久没写博客,写的乱糟糟的,不打算改了,代码比较重要,这些文字不太重要。
参考资料:
https://github.com/zoran-horvat/optional 本文中展示的 Optional 模式的代码实现均来自于 zoran horvat 大佬的 repo,我添加了使用
Nullable
实现的代码作为对比。您可以在我的 repo 中找到:https://github.com/Kit086/kit.demos/tree/main/OptionalPattern;Build Your Own Option Type in C# and Use It Like a Pro - zoran horvat: https://www.youtube.com/watch?v=gpOQl2q0PTU 这是 zoran horvat 对于如何构建 Option 类型的视频讲解,强烈建议订阅他的 Youtube 频道!
0. 前言
我之前写过这篇文章:C# required:跟 string 的空引用异常说再见:https://cat.aiursoft.cn/post/2023/7/18/say-goodbye-to-string-null-reference-exceptions-with-csharps-required-keyword,来尝试部分地解决 null reference 问题。今天这篇文章是使用 Optional 模式来尝试更加彻底地解决这个问题。
1. Null Reference Exception !!!!
写代码这几年,null reference exception 一直是我心里挥之不去的噩梦。不管是进入测试阶段还是修改线上 bug,每次打开日志,十有八九都是满屏的 null reference exception。常规的处理方法是:找到出错的代码位置,加个判断,接着把代码上线,就结束了,危机解除。
![]() |
---|
图 1 - reference meme |
或许有一天,这种忘记进行 null 检查的“小失误”会给我带来大麻烦。如果我平常就能够写出没有 null reference 的代码,这些危机都不用发生,显然生活会变得更加美好。所以今天来探索一下如何避免 null reference exception。
2. Nullable
是永远摆脱空引用异常的方法?
我浏览了视频 这就是永远摆脱空引用异常的方法:https://www.youtube.com/watch?v=v0aB9YCs1oc,它是由 .NET 官方团队的一个大佬讲述的,这是 GPT 的总结:
它介绍了 C# 中新引入的可空引用类型特性,它可以帮助开发者避免空引用异常,提高代码的健壮性和可读性。视频通过演示了如何在代码中使用可空引用类型,以及如何在库和框架中注释可空性,来展示这个特性的优势和注意事项。视频还解释了编译器是如何进行流分析和推断可空性的,以及如何处理泛型、接口和虚方法等情况。最后介绍了如何在项目中启用可空引用类型特性,以及一些常见的问题和解决方案。视频的目的是让开发者了解可空引用类型特性的原理和用法,以及如何在自己的项目中应用它,从而减少空引用异常的发生,提升代码质量。视频的长度是 38 分钟 17 秒。
但这个视频是播客性质的,两个人通过聊天的形式来讲,对于英语一般的人包括我来说,真的很难看下去,半天讲不到重点,扯东扯西,看完了也依然不知道“永远摆脱空引用异常的方法”是什么。并不是说它讲得不好,是我菜了。
在我看来,这个视频实际上在教我们如何使用当时 C# 推出的 Nullable
特性,也就是我们常见的 ?
,也就是这种形式的代码,例如: string? firstName = null
。如果您对此有兴趣,可以浏览这篇博客:https://devblogs.microsoft.com/dotnet/try-out-nullable-reference-types/?WT.mc_id=ondotnet-c9-cxa
但是引入了 Nullable
特性,也就引入了新的问题。从该视频评论就能看得出来:
![]() |
---|
图 2 - 评论 |
翻译过来就是: 我情愿让我的代码上线后炸成渣,被老板炒了鱿鱼,去农场种地,也不想再碰到“可能为空引用的返回”这个烦人的玩意儿。
他至少还能去农场种地,你我有去农场种地的机会吗?
如果你有使用 Nullable
特性的经验,你应该会清楚,如果一个地方出现了 ?
,那么很快,它的上层的调用也会出现一堆 ?
,很快整个项目里就会充满了 ?
,?.
和 ??
,各种各样的 null check 和 null guard。就像病毒在传播一样,很难受。
3. 我们需要什么才能解决因 null 而带来的头痛?
- 我们需要一个安全地访问可为空的引用的方式,以此来一劳永逸地避免空引用问题,让我们不需要在所有的代码中都添加一大堆
?
、?.
、??
等符号来确保引用安全; - 我认为应该由调用者来决定当结果为
null
时该返回什么,这样代码可维护性和可读性都更好。当你有两个高层的方法调用某个底层方法时,对结果为null
时所需要的返回值不同,例如有一个需要返回null
,有一个需要返回string.Empty
,如果调用方可以直接控制,就不需要写多个底层方法或者使用?? string.Empty
这种写法了; - 我希望在可能出现 null reference 异常的地方会直接编译不通过,而不是在 IDE 中的波浪下划线警告。因为很多人是不看警告的,我在很急的时候也常常忽略警告,但这恰恰是 bug 之源;
- 我希望尽可能减少代码中的
null
,甚至干掉业务代码中的null
。我觉得这样会让我的代码人生更加快乐。
4. Optional 模式的实现
我听说 JVM 系列的语言,还有 Rust 等,都使用了 Optional 模式来避免上述的问题。它似乎是来源于函数式编程的一个模式。但 C# 目前还没有内置 Optional 模式的实现,所以我们只能自己写,或者用别的大佬写好的。
https://github.com/zoran-horvat/optional
上面这个 github repo 是 Zoran Horvat 大佬创建的 optional 模式的类和对应的使用示例代码,我们可以在学习完它的用法之后,直接把该 repo 中的 Option.cs
、OptionalExtensions.cs
、ValueOption.cs
复制到我们的项目中使用。
他在 youtube 上也配有视频,介绍了用法和设计这个类的思路:Build Your Own Option Type in C# and Use It Like a Pro:https://www.youtube.com/watch?v=gpOQl2q0PTU
这个仓库包含了使用 C# 实现的 Optional 模式。Optional 模式提供了一种更优雅的方式来处理可空值,避免了使用 null 值。
这个仓库包含了几个实现 Optional 模式的类:
Option.cs
:定义了一个泛型结构体Option<T>
,其中T
是一个引用类型。这个结构体提供了一些方法,如Some
、None
、Map
、MapValue
、MapOptional
、MapOptionalValue
、Reduce
、Where
和WhereNot
,用于创建和操作Option<T>
类型的值。OptionalExtensions.cs
:定义了一些扩展方法,如ToOption
、Where
和WhereNot
,用于将可空引用类型转换为Option<T>
类型的值。ValueOption.cs
:定义了一个泛型结构体ValueOption<T>
,其中T
是一个值类型。这个结构体提供了一些方法,如Some
、None
、Map
、MapValue
、MapOptional
、MapOptionalValue
、Reduce
、Where
和WhereNot
,用于创建和操作ValueOption<T>
类型的值。
与 C# 自带的 Nullable
模式相比,Optional 模式提供了更多的方法来操作可空值。例如,可以使用 Map
方法来对可空值进行转换,使用 Reduce
方法来提供默认值,使用 Where
和 WhereNot
方法来对可空值进行过滤。这些方法可以链式调用,使得代码更加简洁易读。
此外,该代码仓库还提供了 Option<T>
和 ValueOption<T>
两种类型,分别用于处理可空引用类型和可空值类型。这样可以避免使用 Nullable<T>
类型时需要进行装箱和拆箱操作。
这里展示一下 Zoran Horvat 大佬写的 Option.cs
:
public struct Option<T> : IEquatable<Option<T>> where T : class
{
private T? _content;
public static Option<T> Some(T obj) => new() { _content = obj };
public static Option<T> None() => new();
public Option<TResult> Map<TResult>(Func<T, TResult> map) where TResult : class =>
new() { _content = _content is not null ? map(_content) : null };
public ValueOption<TResult> MapValue<TResult>(Func<T, TResult> map) where TResult : struct =>
_content is not null ? ValueOption<TResult>.Some(map(_content)) : ValueOption<TResult>.None();
public Option<TResult> MapOptional<TResult>(Func<T, Option<TResult>> map) where TResult : class =>
_content is not null ? map(_content) : Option<TResult>.None();
public ValueOption<TResult> MapOptionalValue<TResult>(Func<T, ValueOption<TResult>> map) where TResult : struct =>
_content is not null ? map(_content) : ValueOption<TResult>.None();
public T Reduce(T orElse) => _content ?? orElse;
public T Reduce(Func<T> orElse) => _content ?? orElse();
public Option<T> Where(Func<T, bool> predicate) =>
_content is not null && predicate(_content) ? this : Option<T>.None();
public Option<T> WhereNot(Func<T, bool> predicate) =>
_content is not null && !predicate(_content) ? this : Option<T>.None();
public override int GetHashCode() => _content?.GetHashCode() ?? 0;
public override bool Equals(object? other) => other is Option<T> option && Equals(option);
public bool Equals(Option<T> other) =>
_content is null ? other._content is null
: _content.Equals(other._content);
public static bool operator ==(Option<T>? a, Option<T>? b) => a is null ? b is null : a.Equals(b);
public static bool operator !=(Option<T>? a, Option<T>? b) => !(a == b);
}
OptionalExtensions.cs
:
public static class OptionalExtensions
{
public static Option<T> ToOption<T>(this T? obj) where T : class =>
obj is null ? Option<T>.None() : Option<T>.Some(obj);
public static Option<T> Where<T>(this T? obj, Func<T, bool> predicate) where T : class =>
obj is not null && predicate(obj) ? Option<T>.Some(obj) : Option<T>.None();
public static Option<T> WhereNot<T>(this T? obj, Func<T, bool> predicate) where T : class =>
obj is not null && !predicate(obj) ? Option<T>.Some(obj) : Option<T>.None();
}
ValueOption.cs
:
public struct ValueOption<T> : IEquatable<ValueOption<T>> where T : struct
{
private T? _content;
public static ValueOption<T> Some(T obj) => new() { _content = obj };
public static ValueOption<T> None() => new();
public Option<TResult> Map<TResult>(Func<T, TResult> map) where TResult : class =>
_content.HasValue ? Option<TResult>.Some(map(_content.Value)) : Option<TResult>.None();
public ValueOption<TResult> MapValue<TResult>(Func<T, TResult> map) where TResult : struct =>
new() { _content = _content.HasValue ? map(_content.Value) : null };
public Option<TResult> MapOptional<TResult>(Func<T, Option<TResult>> map) where TResult : class =>
_content.HasValue ? map(_content.Value) : Option<TResult>.None();
public ValueOption<TResult> MapOptionalValue<TResult>(Func<T, ValueOption<TResult>> map) where TResult : struct =>
_content.HasValue ? map(_content.Value) : ValueOption<TResult>.None();
public T Reduce(T orElse) => _content ?? orElse;
public T Reduce(Func<T> orElse) => _content ?? orElse();
public ValueOption<T> Where(Func<T, bool> predicate) =>
_content.HasValue && predicate(_content.Value) ? this : ValueOption<T>.None();
public ValueOption<T> WhereNot(Func<T, bool> predicate) =>
_content.HasValue && !predicate(_content.Value) ? this : ValueOption<T>.None();
public override int GetHashCode() => _content?.GetHashCode() ?? 0;
public override bool Equals(object? other) => other is ValueOption<T> option && Equals(option);
public bool Equals(ValueOption<T> other) =>
_content.HasValue ? other._content.HasValue && _content.Value.Equals(other._content.Value)
: !other._content.HasValue;
public static bool operator ==(ValueOption<T> a, ValueOption<T> b) => a.Equals(b);
public static bool operator !=(ValueOption<T> a, ValueOption<T> b) => !(a.Equals(b));
}
使用了 Option Type 的 Person 和 Book 的类:
public class Person
{
public string FirstName { get; }
public Option<string> LastName { get; }
private Person(string firstName, Option<string> lastName) =>
(FirstName, LastName) = (firstName, lastName);
public static Person Create(string firstName) =>
new(firstName, Option<string>.None());
public static Person Create(string firstName, string lastName) =>
new(firstName, Option<string>.Some(lastName));
public override string ToString() =>
this.LastName
.Map(lastName => $"{FirstName} {lastName}")
.Reduce(FirstName);
}
public class Book
{
public string Title { get; }
public Option<Person> Author { get; }
private Book(string title, Option<Person> author) =>
(Title, Author) = (title, author);
public static Book Create(string title) =>
new(title, Option<Person>.None());
public static Book Create(string title, Person author) =>
new(title, Option<Person>.Some(author));
public override string ToString() =>
Author.Map(author => $"{Title} by {author}").Reduce(Title);
}
如果没有 Option
,使用 Nullable
模式的话,Person
类的 public Option<string> LastName { get; }
属性应该会是 public string? LastName { get; }
,Book
类的 public Option<Person> Author { get; }
属性应该会是 public Person? Author { get; }
。不用我说,您也应该能想到后续对这两个类使用的时候,要加多少 ?
、?.
和 ??
操作符了,可能还会有 !
。
这是我写的如果没有使用 Option
而是使用 Nullable
的 Book
和 Person
类的代码,分别命名为 NullableBook
和 NullablePerson
。这个命名显然会产生歧义,但是为了看起来分明,所以我还是这样命名了:
public class NullableBook
{
public string Title { get; }
public NullablePerson? Author { get; }
private NullableBook(string title, NullablePerson? author) =>
(Title, Author) = (title, author);
public static NullableBook Create(string title) =>
new(title, null);
public static NullableBook Create(string title, NullablePerson author) =>
new(title, author);
public override string ToString() =>
this.Author is not null
? $"{Title} by {Author}"
: Title;
}
public class NullablePerson
{
public string FirstName { get; }
public string? LastName { get; }
private NullablePerson(string firstName, string? lastName) =>
(FirstName, LastName) = (firstName, lastName);
public static NullablePerson Create(string firstName) =>
new(firstName, null);
public static NullablePerson Create(string firstName, string lastName) =>
new(firstName, lastName);
public override string ToString() =>
!string.IsNullOrWhiteSpace(this.LastName)
? $"{FirstName} {LastName}"
: FirstName;
}
使用 Option
的示例代码:
Person mann = Person.Create("Thomas", "Mann");
Person aristotle = Person.Create("Aristotle");
Person austen = Person.Create("Jane", "Austen");
Person asimov = Person.Create("Isaac", "Asimov");
Person marukami = Person.Create("Haruki", "Murakami");
Book faustus = Book.Create("Doctor Faustus", mann);
Book rhetoric = Book.Create("Rhetoric", aristotle);
Book nights = Book.Create("One Thousand and One Nights");
Book foundation = Book.Create("Foundation", asimov);
Book robots = Book.Create("I, Robot", asimov);
Book pride = Book.Create("Pride and Prejudice", austen);
Book mahabharata = Book.Create("Mahabharata");
Book windup = Book.Create("Windup Bird Chronicle", marukami);
IEnumerable<Book> library = new[] { faustus, rhetoric, nights, foundation, robots, pride, mahabharata, windup };
var bookshelf = library
.GroupBy(GetAuthorInitial)
.OrderBy(group => group.Key.Reduce(string.Empty));
foreach (var group in bookshelf)
{
string header = group.Key.Map(initial => $"[ {initial} ]").Reduce("[ ]");
foreach (var book in group)
{
Console.WriteLine($"{header} -> {book}");
header = " ";
}
}
Console.WriteLine(new string('-', 40));
var authorNameLengths = library
.GroupBy(GetAuthorNameLength)
.OrderBy(group => group.Key.Reduce(0));
foreach (var group in authorNameLengths)
{
string header = group.Key.Map(length => $"[ {length,2} ]").Reduce("[ ]");
foreach (var book in group)
{
Console.WriteLine($"{header} -> {book}");
header = " ";
}
}
ValueOption<int> GetAuthorNameLength(Book book) =>
book.Author.Map(GetName).MapValue(s => s.Length);
string GetName(Person person) =>
person.LastName
.Map(lastName => $"{person.FirstName} {lastName}")
.Reduce(person.FirstName);
Option<string> GetAuthorInitial(Book book)
{
return book.Author.MapOptional(GetPersonInitial);
}
Option<string> GetPersonInitial(Person person) =>
person.LastName
.MapValue(GetInitial)
.Reduce(() => GetInitial(person.FirstName));
Option<string> GetInitial(string name) =>
name.WhereNot(string.IsNullOrWhiteSpace)
.Map(s => s.TrimStart().Substring(0, 1).ToUpper());
如果不使用 Option,那么上面这个例子中的代码应该是这样的:
NullablePerson mann = NullablePerson.Create("Thomas", "Mann");
NullablePerson aristotle = NullablePerson.Create("Aristotle");
NullablePerson austen = NullablePerson.Create("Jane", "Austen");
NullablePerson asimov = NullablePerson.Create("Isaac", "Asimov");
NullablePerson marukami = NullablePerson.Create("Haruki", "Murakami");
NullableBook faustus = NullableBook.Create("Doctor Faustus", mann);
NullableBook rhetoric = NullableBook.Create("Rhetoric", aristotle);
NullableBook nights = NullableBook.Create("One Thousand and One Nights");
NullableBook foundation = NullableBook.Create("Foundation", asimov);
NullableBook robots = NullableBook.Create("I, Robot", asimov);
NullableBook pride = NullableBook.Create("Pride and Prejudice", austen);
NullableBook mahabharata = NullableBook.Create("Mahabharata");
NullableBook windup = NullableBook.Create("Windup Bird Chronicle", marukami);
IEnumerable<NullableBook> library = new[] { faustus, rhetoric, nights, foundation, robots, pride, mahabharata, windup };
var author = GetAuthorInitial(rhetoric);
Console.WriteLine(author);
var bookshelf = library
.GroupBy(GetAuthorInitial)
.OrderBy(group => group.Key ?? string.Empty);
foreach (var group in bookshelf)
{
string header = !string.IsNullOrWhiteSpace(group.Key)? $"[ {group.Key} ]" : "[ ]";
foreach (var book in group)
{
Console.WriteLine($"{header} -> {book}");
header = " ";
}
}
Console.WriteLine(new string('-', 40));
var authorNameLengths = library
.GroupBy(GetAuthorNameLength)
.OrderBy(group => group.Key ?? 0);
foreach (var group in authorNameLengths)
{
string header = group.Key is not null ? $"[ {group.Key,2} ]" : "[ ]";
foreach (var book in group)
{
Console.WriteLine($"{header} -> {book}");
header = " ";
}
}
int? GetAuthorNameLength(NullableBook book) =>
book.Author is not null
? GetName(book.Author).Length
: null;
string GetName(NullablePerson person) =>
person.LastName is not null
? $"{person.FirstName} {person.LastName}"
: person.FirstName;
string? GetAuthorInitial(NullableBook book) =>
book.Author is not null && !string.IsNullOrWhiteSpace(book.Author.LastName)
? GetPersonInitial(book.Author)
: book.Author is not null && !string.IsNullOrWhiteSpace(book.Author.FirstName)
? GetPersonInitial(book.Author)
: null;
string? GetPersonInitial(NullablePerson person) =>
!string.IsNullOrWhiteSpace(person.LastName)
? GetInitial(person.LastName)
: GetInitial(person.FirstName);
string? GetInitial(string name) =>
name?.TrimStart()?[..1]?.ToUpper();
这些使用 Nullable
的代码是我自己添加的,您可以在我的 repo 中找到:https://github.com/Kit086/kit.demos/tree/main/OptionalPattern;
5. Optional 模式相对于 C# 的 Nullable 特性的优势在哪?
您可以对比 Zoran Horvat 与我的代码,来查看 Optional 模式和 Nullable 模式的区别,来选择您更喜欢的方式。
看起来,Optional 模式导致代码写起来更加复杂了,可读性也并没有变好多少,那它的优点是什么呢?
上一个小节 4. Optional 模式 中已经穿插讲过了它的部分优点,这里说一下我体会到的优势:
示例代码中,没有一个 null
。我们不在方法中传递 null
,就基本上避免了 null reference 异常了,会很省心,不用每次都担忧是不是又忘了检查 null
了。对于 Optional 的对象,当它不存在的时候,根本不会发生调用,也就不用担心调用某个方法会返回 null
了。
而且我在 3. 我们需要什么才能解决因 null 而产生的头痛? 这一小节中提到的需要解决的问题,Optional 模式也全都解决了!
但是 Optional 模式写起来感觉稍微绕一些,可能是因为我并不熟悉函数式编程。
虽然有小缺点,但让我们设想一种情况:“如果我们急着上线项目而没有写单元测试集成测试,又忽视了 IDE 的警告,从而忘记进行 null check,导致 null reference exception“。这种常见的情况,使用 Optional 模式就可以规避,如果有 null reference exception,它不会报警告,而是会直接无法通过静态编译检查。
6. 总结
Nullable
和 Optional 模式,如果让我选择,我可能会根据项目的大小,参与项目的成员的水平等因素来决定使用哪种方法,但它们都是不错的 null reference 的解决方案。
在探讨如何有效处理C#中的空值问题时,文章对比了Nullable特性和Optional模式两种解决方案,深入分析了各自的优缺点以及适用场景。以下是对文章内容的总结和进一步讨论:
Nullable特性
Optional模式
实际应用中的考虑
项目规模与团队能力:
开发实践:
风险与保障:
结论
选择Nullable特性还是Optional模式取决于项目的具体情况和团队偏好。Optional模式在提高代码健壮性和可读性方面有显著优势,但需要一定的学习曲线和调整成本。实践中,可以结合两者的优势,根据项目需求灵活选择解决方案。通过实际应用中的经验积累,才能更好地判断哪种方法最适合当前的开发环境。
使用Option确实可以让病毒式的?,??消失,但是会引入病毒式的Option
...接上文
原文:我希望尽可能减少代码中的 null,甚至干掉业务代码中的 null。我觉得这样会让我的代码人生更加快乐。
您的理解是完全正确的。这正是nullable的目标。
回答你的问题:
是的,你说的没错。那么就开启nullable,然后忘记null这个关键字,永远不要返回null。
你的理解部分正确。但是你的例子不太对。如果你的方法返回string,那么你的方法应该返回string,而不是null。如果你的方法返回string?,那么你的方法应该返回string?,告诉调用者:这是个危险的方法,调用它,你最好检查我的返回值,因为我自己都不知道为什么我就返回null了。
如果你一定认为null是一个合法的结果,那么你应该返回 Class? 而不是 Class。这样代码会看起来很丑,但这不是nullable的问题,是开发者根本就不应该返回null的问题。
总有人询问我:例如开发了一个函数,可以根据ID查找用户,如果找到了就返回用户,如果没找到就返回null,这样的函数怎么办?我会告诉他:
要么你的函数应该返回User?,而不是User。如果你的函数返回User,那么你的函数应该返回一个User,而不是null。如果一定需要考虑没有查找到的时候,你可以使用博客中提到的Option<User>,或使用TryGetUser的方式,或你自己封装一个 QueryResult,由FoundResult和NotFoundResult去继承。
这个我同意。编译选项完全支持你将特定的warning视为error,而不是只有nullability的warning。你可以逼迫开发者必须处理所有的warning,而不是只有nullability的warning。
我希望尽可能减少代
所以你的文章并没有理解nullable的精髓。回答这个部分:
这是故意这么设计的!引用 Nullable 必须要让人重新思考那些“好好的代码怎么就return开null了”的问题!避免使用null,避免在异常情况下返回null,并且在发现null时提早返回exception。
如果不思考,那么就关闭Nullable这个功能!
你引用的评论明显就没有理解nullable的精髓和目的所在。它并不是一个可以快速打开,就瞬间解决所有问题的开关。nullable的目的是给每个开发者对自己的代码多了一次反省的机会:为什么它要返回一个没有意义的值?如果你觉得它不应该是null,就应该从最根上把它修了。让它直接返回不可空的类型。给我nullable类型,那么这其实是违反这个世界的直觉的。开启nullable需要开发者重新思考每一个函数,如果那个评论者不理解这个问题,自然他会觉得这个开关一旦打开就全是问题。
如果你觉得这个变量是别的sdk给我的,但是我明确知道这个类型一定不会是null,那就应该.Value来将其转换为不可为null类型。
这个时候你就会想:妈的,我一个.Value过去,万一真是null了不就炸了?好事儿啊,nullable就是为了让你提早炸,发现不应该是null的东西变成null了,第一时间炸,而不是把null传出去。因此nullable的精髓就是你可以看到这个警告信息:你的函数可能返回null!
一旦你忽视警告,把null传到了上层,那么又回到了满世界都需要判断你的返回值是否为null了。
有人批评nullable逼你加一大堆 ? ?. ??,不能痛快的写.了,这不正是给了人一次重新思考的机会:为什么这个世界明明规则是简单的,偏偏搞出来一种null这个东西?在开启nullable的世界,如果你不使用Class?,那么上null就不是一个合法的返回值。nullable打开是逼迫程序员重新思考每一个方法是不是可能返回null。无脑全部加一个 ? 反而享受不到nullable带来的这种“逼迫”。
而如果没人去反思自己的方法为什么返回了 null,没有这种逼迫存在,我们不仍然生活在一个到处担心null的世界里么?在一个开启nullable的项目里,你应当非常信任一个不可为null的值,不要对它进行空指针检查:它永远都不可能为null。而你也不应该写返回null的代码。在一个理想的世界里,nullable开启以后,这个世界上就特么不应该有null这个关键字了。
问题的根源还是Tony的Million Dollar Mistake。nullable是在试图解决它。
很久没写博客,写的乱糟糟的,不打算改了,代码比较重要,这些文字不太重要
首先,我想赞扬你对C#中空引用问题的深入探讨。你的文章详细地阐述了问题,并提出了两种可能的解决方案:使用
Nullable
特性和Optional模式。你的文章内容丰富,逻辑清晰,使得读者能够深入了解这个问题和可能的解决方案。你的文章最大的闪光点在于你对Optional模式的深入探讨。你详细地介绍了如何在C#中实现Optional模式,并且提供了相关的资源链接,使得读者能够进一步了解和学习Optional模式。此外,你对Optional模式的优点进行了详细的分析,使得读者能够了解到Optional模式相对于
Nullable
特性的优势。你的文章在逻辑上没有明显的错误,你的观点都是基于事实和你的经验。然而,你的文章可以在一些地方进行改进。首先,你在讨论
Nullable
特性时,主要依赖于视频的内容,然后你提到因为视频的语言问题,你没有完全理解视频的内容。我建议你可以找一些文字的资料,如博客、教程或者文档,来更深入地了解Nullable
特性。其次,你在讨论Optional模式时,提供了大量的代码和资源链接,但是没有提供具体的代码示例。我建议你可以提供一些简单的代码示例,来展示如何在C#中使用Optional模式。总的来说,你的文章是一篇非常好的博客,它提供了深入的分析和有价值的资源。我期待你在未来的文章中继续分享你的知识和经验。