名称: csharp-nullable-types 用户可调用: false 描述: 当C#可空引用类型和值类型用于空安全性、可空注解和处理空值的模式时使用。 允许的工具:
- 读取
- 写入
- 编辑
- Grep
- Glob
- Bash
C# 可空类型
C#中的可空类型有助于防止空引用异常,这是最常见的编程错误之一。C# 8.0引入了可空引用类型,提供了编译时空安全性检查。此技能涵盖可空值类型、可空引用类型、空合并运算符以及安全处理空值的模式。
可空值类型
值类型(int、double、bool、struct)通常不能为null。可空值类型使得能够表示“无值”状态。
using System;
public class NullableValueTypes
{
// 声明
public void Declaration()
{
int? nullableInt = null;
int? anotherInt = 42;
double? nullableDouble = null;
bool? nullableBool = true;
// 通用语法(等价)
Nullable<int> genericNullable = null;
}
// 检查空值
public void NullChecking()
{
int? value = GetNullableValue();
// HasValue属性
if (value.HasValue)
{
int actualValue = value.Value;
Console.WriteLine(actualValue);
}
// 与null比较
if (value != null)
{
Console.WriteLine(value.Value);
}
// 空条件运算符
int? doubled = value?.GetHashCode();
}
// 获取值
public void GettingValues()
{
int? value = 42;
// Value属性(如果为null则抛出异常)
int val1 = value.Value;
// GetValueOrDefault
int val2 = value.GetValueOrDefault(); // 如果为null返回0
int val3 = value.GetValueOrDefault(10); // 如果为null返回10
// 空合并运算符
int val4 = value ?? 0; // 如果为null返回0
}
// 可空表达式
public void NullableExpressions()
{
int? a = 10;
int? b = 20;
int? c = null;
// 算术运算(如果任何操作数为null则结果为null)
int? sum1 = a + b; // 30
int? sum2 = a + c; // null
// 比较
bool? equal = a == b; // false
bool? greater = a > c; // null
// 逻辑运算符
bool? and = (a > 5) & (c > 5); // null
}
// 可空结构
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
public void NullableStructs()
{
Point? point = null;
if (point.HasValue)
{
int x = point.Value.X;
int y = point.Value.Y;
}
// 结构体的空条件运算符
int? x = point?.X;
}
private int? GetNullableValue() => null;
}
可空引用类型
C# 8.0+提供了可空引用类型,使得引用类型在编译时具有空安全性。
#nullable enable
using System;
using System.Collections.Generic;
public class NullableReferenceTypes
{
// 默认非空
public string Name { get; set; } = string.Empty;
// 显式可空
public string? MiddleName { get; set; }
// 构造函数
public NullableReferenceTypes(string name, string? middleName = null)
{
Name = name; // 必须非空
MiddleName = middleName; // 可以为空
}
// 带有可空参数的方法
public string FormatName(string firstName, string? middleName,
string lastName)
{
// 如果未进行空检查使用middleName,编译器会警告
if (middleName != null)
{
return $"{firstName} {middleName} {lastName}";
}
return $"{firstName} {lastName}";
}
// 可空返回类型
public string? FindPerson(int id)
{
// 可能返回null
return id > 0 ? "Found" : null;
}
// 可空类型的集合
public void CollectionExamples()
{
// 非空字符串列表
List<string> names = new List<string> { "Alice", "Bob" };
// 可空字符串列表
List<string?> nullableNames = new List<string?>
{
"Alice", null, "Charlie"
};
// 带有可空值的字典
Dictionary<string, string?> dict =
new Dictionary<string, string?>
{
{ "key1", "value1" },
{ "key2", null }
};
}
// 可空泛型类型
public T? FindById<T>(int id) where T : class
{
// 如果未找到则返回null
return default(T);
}
// 非空断言运算符
public void NotNullAssertion()
{
string? maybeName = GetName();
// 告诉编译器这不会为空(谨慎使用!)
string name = maybeName!;
}
private string? GetName() => null;
}
空合并运算符
空合并运算符提供简洁的空值处理。
#nullable enable
using System;
using System.Collections.Generic;
public class NullCoalescingOperators
{
// 空合并运算符 (??)
public string GetDisplayName(string? name)
{
// 如果name为null则返回"Unknown"
return name ?? "Unknown";
}
// 链式
public string GetFirstNonNull(string? a, string? b, string? c)
{
return a ?? b ?? c ?? "Default";
}
// 空合并赋值运算符 (??=)
public void NullCoalescingAssignment()
{
string? name = null;
// 仅当为null时赋值
name ??= "Default Name"; // name现在为"Default Name"
name ??= "Another Name"; // name保持为"Default Name"
}
// 与集合
public void CollectionCoalescing()
{
List<string>? list = null;
// 如果为null则初始化
list ??= new List<string>();
// 添加项目
list.Add("item");
}
// 在属性初始化中
private List<string>? _items;
public List<string> Items => _items ??= new List<string>();
// 复杂表达式
public string ComplexCoalescing(User? user)
{
// 多个层级
return user?.Profile?.DisplayName ??
user?.Email ??
"Guest";
}
public class User
{
public Profile? Profile { get; set; }
public string? Email { get; set; }
}
public class Profile
{
public string? DisplayName { get; set; }
}
}
空条件运算符
空条件运算符安全地访问可能为空的引用的成员和调用方法。
#nullable enable
using System;
using System.Collections.Generic;
public class NullConditionalOperator
{
public class Person
{
public string Name { get; set; } = string.Empty;
public Address? Address { get; set; }
public List<string>? PhoneNumbers { get; set; }
}
public class Address
{
public string City { get; set; } = string.Empty;
public string? PostalCode { get; set; }
}
// 基本空条件
public string? GetCity(Person? person)
{
// 如果person或Address为null则返回null
return person?.Address?.City;
}
// 与索引器
public string? GetFirstPhone(Person? person)
{
// 如果person、PhoneNumbers为null或空则返回null
return person?.PhoneNumbers?[0];
}
// 与方法调用
public int? GetNameLength(Person? person)
{
return person?.Name.Length;
}
// 结合运算符
public string GetCityOrDefault(Person? person)
{
return person?.Address?.City ?? "Unknown";
}
// 与数组
public string? GetFirstItem(string[]? items)
{
return items?[0];
}
// 与委托
public void InvokeCallback(Action? callback)
{
callback?.Invoke();
}
// 与事件
public event EventHandler? DataChanged;
public void OnDataChanged()
{
DataChanged?.Invoke(this, EventArgs.Empty);
}
// 链式多个操作
public void ChainedOperations(Person? person)
{
string? result = person?
.Address?
.City
.ToUpper()
.Substring(0, 3);
}
}
空值处理模式
安全处理空值的常见模式。
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
public class NullHandlingPatterns
{
// 空对象模式
public interface ILogger
{
void Log(string message);
}
public class ConsoleLogger : ILogger
{
public void Log(string message) =>
Console.WriteLine(message);
}
public class NullLogger : ILogger
{
public void Log(string message) { }
}
// 保护子句
public void ProcessData(string? data)
{
if (data == null)
{
throw new ArgumentNullException(nameof(data));
}
// 此处data非空
Console.WriteLine(data.Length);
}
// 提前返回
public string FormatData(string? data)
{
if (data == null) return string.Empty;
return data.ToUpper();
}
// TryGet模式
public bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
{
// 模拟查找
if (key == "valid")
{
value = "Found";
return true;
}
value = null;
return false;
}
public void UseTryGet()
{
if (TryGetValue("valid", out string? result))
{
// 此处result保证非空
Console.WriteLine(result.Length);
}
}
// MemberNotNull属性
private string? _cachedValue;
[MemberNotNull(nameof(_cachedValue))]
private void EnsureCacheLoaded()
{
_cachedValue ??= LoadValue();
}
public void UseCache()
{
EnsureCacheLoaded();
// _cachedValue保证非空
Console.WriteLine(_cachedValue.Length);
}
// NotNullIfNotNull属性
[return: NotNullIfNotNull(nameof(input))]
public string? Transform(string? input)
{
return input?.ToUpper();
}
// AllowNull和DisallowNull
private string _name = string.Empty;
[AllowNull]
public string Name
{
get => _name;
set => _name = value ?? string.Empty;
}
private string LoadValue() => "loaded";
}
可空注解
为编译器提供额外空状态信息的属性。
#nullable enable
using System.Diagnostics.CodeAnalysis;
using System.Collections.Generic;
public class NullableAnnotations
{
// NotNull - 参数在方法返回时不会为空
public void Initialize([NotNull] ref string? value)
{
value ??= "default";
}
// MaybeNull - 尽管返回类型非空,但可能返回null
[return: MaybeNull]
public T GetDefaultValue<T>()
{
return default(T);
}
// DoesNotReturn - 方法从不正常返回
[DoesNotReturn]
public void ThrowError(string message)
{
throw new System.InvalidOperationException(message);
}
public void UseDoesNotReturn(string? value)
{
if (value == null)
{
ThrowError("Value is null");
}
// 编译器知道此处value非空
System.Console.WriteLine(value.Length);
}
// MemberNotNullWhen
private string? _data;
[MemberNotNullWhen(true, nameof(_data))]
private bool IsDataLoaded()
{
return _data != null;
}
public void UseData()
{
if (IsDataLoaded())
{
// _data保证非空
System.Console.WriteLine(_data.Length);
}
}
// NotNullIfNotNull
[return: NotNullIfNotNull(nameof(source))]
public string? ProcessString(string? source)
{
return source?.Trim();
}
}
迁移策略
在现有代码中采用可空引用类型的策略。
// 渐进迁移方法
// 文件1:保持可空性禁用
#nullable disable
public class LegacyClass
{
public string Name { get; set; } // 隐式可空
}
#nullable restore
// 文件2:为新代码启用可空性
#nullable enable
public class ModernClass
{
public string Name { get; set; } = string.Empty; // 非空
public string? MiddleName { get; set; } // 显式可空
}
#nullable restore
// 文件3:迁移期间抑制警告
#nullable enable
public class MigratingClass
{
// 抑制特定警告
#pragma warning disable CS8618
public string Name { get; set; }
#pragma warning restore CS8618
// 或临时使用!运算符
public string GetName() => Name!;
}
与外部库协作
处理未使用可空注解的库时的可空性。
#nullable enable
using System;
public class ExternalLibraryHandling
{
// 假设外部库方法
// public string GetData() { ... }
// 防御性编程
public void UseExternalLibrary()
{
// 将返回值视为可能为空
string? data = GetExternalData();
if (data != null)
{
ProcessData(data);
}
}
// 创建包装器
public string? GetExternalDataSafe()
{
string result = GetExternalData();
return string.IsNullOrEmpty(result) ? null : result;
}
// 安全扩展方法
public static class StringExtensions
{
public static string OrEmpty(this string? value)
{
return value ?? string.Empty;
}
public static bool IsNullOrEmpty(
[NotNullWhen(false)] this string? value)
{
return string.IsNullOrEmpty(value);
}
}
private string GetExternalData() => "data";
private void ProcessData(string data) { }
}
最佳实践
- 在新项目中从一开始就启用可空引用类型
- 对可选字符串参数和返回值使用
string? - 在构造函数中或用默认值初始化非空属性
- 优先使用空合并运算符而非显式空检查
- 为
TryGet模式方法使用NotNullWhen属性 - 早期用保护子句验证参数
- 使用
ThrowIfNull助手进行参数验证 - 除非绝对确定值非空,否则避免使用
!运算符 - 利用编译器警告发现潜在的空引用问题
- 在公共API中记录可空性预期
常见陷阱
- 使用
!运算符抑制警告而不确保非空 - 未在所有构造函数中初始化非空属性
- 从声明为非空返回类型的方法返回null
- 在解引用可空引用前忘记检查空值
- 混合可空和非空上下文导致混淆
- 过度使用可空类型而默认值已足够
- 未通过方法链传播空检查
- 假设外部库方法尊重可空性
- 迁移期间忽略可空警告
- 过度使用
#pragma warning disable
何时使用可空类型
当需要时使用可空类型:
- 表示无值状态,区别于默认值
- 数据库可为NULL的字段映射到C#属性
- 方法和构造函数中的可选参数
- 可能包含或不包含数据的API响应
- 在编译时防止空引用异常
- 将旧代码逐步迁移到空安全模式
- 关于哪些值可以为空的清晰契约
- 可选配置值的类型安全处理
- 与可能返回null的外部API集成
- 带有Option/Maybe类型的函数式编程模式