“可空引用类型的目标是防止空引用异常”
可空值类型与可空引用类型
在C# 2.0之前,只有引用类型是可为null的。值类型,例如int或DateTime不能是null 。如果这些类型在没有值的情况下初始化,则它们将回退到其默认值。例如int是0,DateTime是DateTime.MinValue。常用的值类型:byte,short,int,long,float,double,decimal,char,bool,enum和struct。
string first; // first为null,是因为已声明引用类型string,但未进行赋值
string second = string.Empty; // second在声明时分配string.Empty
int third; // third尽管没有被分配。它是一个值类型,默认值为0
DateTime date; // date未初始化,但其默认值为System.DateTime.MinValue
从C# 2.0开始,可使用泛型类型System.Nullable(或T?速记)定义可为null的值类型,其中T就是可替换的值类型。
int? first; // first是null是因为可为null的值类型未初始化
int? second = null; // second在声明时分配null
int? third = default; // third为null是因为Nullable<int>默认值是null
int? fourth = new(); // fourth是0是因为new()表达式调用Nullable<int>构造函数时,默认int为0
C# 8.0引入了可为null的引用类型,你可以表达引用类型可能是null
或始终是非null
的意图。
在C# 8.0之前,所有引用类型默认情况下可以是null,并且编译器不会警告你未检查的null赋值或使用。C# 8.0引入了可为空的引用类型(nullable reference types),使得你可以在类型系统中明确地表示某个引用类型是否允许为null。这有助于减少空引用异常(NullReferenceException)并提高代码的安全性和可维护性。
- 如果你在类型声明后加上?,例如string?,这意味着这个引用类型可以是null。
- 如果你不加?,例如string,在启用可为空引用类型的情况下,编译器会假设这个引用类型不应该是null,并在可能为null的情况下给出警告。
#nullable enable
string first = string.Empty;//first从来都不是null,因为它被明确分配
string second; //second不应该是null
string? third; //third可能是null,例如,它可能指向System.String,但也可能指向null。这些中的任何一种都是可以接受的
务必注意,可为null的引用类型与可为null的值类型不是一回事。可为null的值类型映射到.NET中的具体类类型。所以int?
实际上是Nullable<int>
。但是对于string?
,它实际上是相同的string
,但有一个编译器生成的属性来注释它。这样做是为了向后兼容。换句话说,string?
是一种“假类型”,而int?
不是。
应用举例
变量在为Null时返回的一个指定的值
namespace Demo
{
internal class Program
{
static void Main(string[] args)
{
double? numNull = null;
double? numNotNull = 3.14157;
double num;
num = numNull ?? 5.34;
Console.WriteLine("num的值:{0}", num); //num的值:5.34
num = numNotNull ?? 5.34;
Console.WriteLine("num的值:{0}", num); //num的值:3.14157
Console.ReadLine();
}
}
}
可空值类型的值赋给普通类型
编译器不允许将一个可空值类型的值直接赋值给普通的值类型,例如下面的代码是不能通过编译的:
int? myNullabelInteger=null;
int myInteger=myNullabelInteger;
因为可空值类型的值可以是null,而null不可以赋给普通值类型。
将一个可空值类型的值赋给普通类型的方法是:先用System.Nullable<T>.Hasvalue
属性判断这个可空值类型是否被赋过一个类型为T的有效值:
namespace Demo
{
internal class Program
{
static void Main(string[] args)
{
int? myNullableInteger = null;
if (myNullableInteger.HasValue)
{
int myInteger = myNullableInteger.Value;
}
else
{
Console.WriteLine("myNullableInteger尚未被赋非null值");
}
myNullableInteger = 666;
if (myNullableInteger.HasValue)
{
int myInteger = myNullableInteger.Value;
Console.WriteLine($"myNullableInteger赋值{myInteger},并将myNullableInteger赋值给普通值类型的myInteger");
}
Console.ReadLine();
}
}
}
输出结果:
myNullableInteger尚未被赋非null值
myNullableInteger赋值666,并将myNullableInteger赋值给普通值类型的myInteger
数据转换为指定类型
将一个object类型的值转换为任何可能转换的类型
using System.ComponentModel;
public static class BaseHelper
{
public static object CusChanageType(this object value, Type convertsionType)
{
//判断convertsionType类型是否为泛型,因为nullable是泛型类。判断convertsionType是否为nullable泛型类
if (convertsionType.IsGenericType && convertsionType.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
{
if (value == null || value.ToString().Length == 0)
{
return null;
}
//如果convertsionType为nullable类,声明一个NullableConverter类,该类提供从Nullable类到基础基元类型的转换
NullableConverter nullableConverter = new NullableConverter(convertsionType);
//将convertsionType转换为nullable对的基础基元类型
convertsionType = nullableConverter.UnderlyingType;
}
return Convert.ChangeType(value, convertsionType);
}
}
namespace Demo
{
internal class Program
{
static void Main(string[] args)
{
// 示例1:将字符串转换为可空整数
object value1 = "123";
Type type1 = typeof(int?);
object result1 = value1.CusChanageType(type1);
Console.WriteLine(result1); // 输出: 123
// 示例2:将null转换为可空整数
object value2 = null;
Type type2 = typeof(int?);
object result2 = value2.CusChanageType(type2);
Console.WriteLine(result2 == null); // 输出: True
// 示例3:将字符串转换为非可空整数
object value3 = "456";
Type type3 = typeof(int);
object result3 = value3.CusChanageType(type3);
Console.WriteLine(result3); // 输出: 456
// 示例4:将字符串转换为非可空浮点数
object value4 = "789.01";
Type type4 = typeof(double);
object result4 = value4.CusChanageType(type4);
Console.WriteLine(result4); // 输出: 789.01
// 示例5:将空字符串转换为可空浮点数
object value5 = "";
Type type5 = typeof(double?);
object result5 = value5.CusChanageType(type5);
Console.WriteLine(result5 == null); // 输出: True
// 示例6:将布尔值字符串转换为布尔类型
object value6 = "true";
Type type6 = typeof(bool);
object result6 = value6.CusChanageType(type6);
Console.WriteLine(result6); // 输出: True
}
}
}
继续给出一个更加贴近实际项目的例子。项目开发中经常从配置文件
读取配置信息(这里省去从文件中读取配置,直接将配置文件信息初始化到configValues字典中)
using System.ComponentModel;
using System;
using System.Collections.Generic;
public class ConfigReader
{
private readonly Dictionary<string, string> _configValues;
public ConfigReader(Dictionary<string, string> configValues)
{
_configValues = configValues;
}
public T GetConfigValue<T>(string key)
{
if (_configValues.TryGetValue(key, out var value))
{
return (T)value.CusChanageType(typeof(T));
}
return default(T);
}
}
public class AppConfig
{
public int? MaxRetries { get; set; }
public double? Timeout { get; set; }
public bool EnableLogging { get; set; }
public string LogLevel { get; set; }
}
public static class BaseHelper
{
public static object CusChanageType(this object value, Type convertsionType)
{
//判断convertsionType类型是否为泛型,因为nullable是泛型类。判断convertsionType是否为nullable泛型类
if (convertsionType.IsGenericType && convertsionType.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
{
if (value == null || value.ToString().Length == 0)
{
return null;
}
//如果convertsionType为nullable类,声明一个NullableConverter类,该类提供从Nullable类到基础基元类型的转换
NullableConverter nullableConverter = new NullableConverter(convertsionType);
//将convertsionType转换为nullable对的基础基元类型
convertsionType = nullableConverter.UnderlyingType;
}
return Convert.ChangeType(value, convertsionType);
}
}
namespace Demo
{
internal class Program
{
static void Main(string[] args)
{
// 初始化配置字典
var configValues = new Dictionary<string, string>
{
{ "MaxRetries", "5" },
{ "Timeout", "30.5" },
{ "EnableLogging", "true" },
{ "LogLevel", "info" }
};
var configReader = new ConfigReader(configValues);
var config = new AppConfig
{
MaxRetries = configReader.GetConfigValue<int?>("MaxRetries"),
Timeout = configReader.GetConfigValue<double?>("Timeout"),
EnableLogging = configReader.GetConfigValue<bool>("EnableLogging"),
LogLevel = configReader.GetConfigValue<string>("LogLevel")
};
// 输出读取的配置项
Console.WriteLine($"MaxRetries: {config.MaxRetries}");
Console.WriteLine($"Timeout: {config.Timeout}");
Console.WriteLine($"EnableLogging: {config.EnableLogging}");
Console.WriteLine($"LogLevel: {config.LogLevel}");
}
}
}
程序输出:
MaxRetries: 5
Timeout: 30.5
EnableLogging: True
LogLevel: info
反射来确定属性是否是一个可空类型
这个示例展示了如何使用反射来确定一个属性是否是Nullable<>类型,并获取其基础类型。在实际应用中,这种方法可以用于动态处理类型信息,适用于很多需要自动化处理属性类型的场景。
using System;
using System.Reflection;
public class NullableTest
{
public int? Id { get; set; }
public int Age { get; set; }
}
class Program
{
static void Main()
{
var idPropertyInfo = typeof(NullableTest).GetProperty("Id");
var agePropertyInfo = typeof(NullableTest).GetProperty("Age");
if (idPropertyInfo.PropertyType.IsGenericType && idPropertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
Type[] typeArray = idPropertyInfo.PropertyType.GetGenericArguments();
Type baseType = typeArray[0];//泛型参数数组中的第一个(也是唯一一个)元素即为基础类型
//如果属性是可空类型,输出属性名称和基础类型
Console.WriteLine($"Property '{idPropertyInfo.Name}' is a nullable type.");
Console.WriteLine($"Base type of '{idPropertyInfo.Name}' is {baseType}.");
}
else
{
//如果属性不是可空类型,输出相应信息
Console.WriteLine($"Property '{idPropertyInfo.Name}' is not a nullable type.");
}
if (agePropertyInfo.PropertyType.IsGenericType && agePropertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
Type[] typeArray = agePropertyInfo.PropertyType.GetGenericArguments();
Type baseType = typeArray[0];//泛型参数数组中的第一个(也是唯一一个)元素即为基础类型
//如果属性是可空类型,输出属性名称和基础类型
Console.WriteLine($"Property '{agePropertyInfo.Name}' is a nullable type.");
Console.WriteLine($"Base type of '{agePropertyInfo.Name}' is {baseType}.");
}
else
{
//如果属性不是可空类型,输出相应信息
Console.WriteLine($"Property '{agePropertyInfo.Name}' is not a nullable type.");
}
}
}
Property 'Id' is a nullable type.
Base type of 'Id' is System.Int32.
Property 'Age' is not a nullable type.
AllowNull
AllowNull属性用于指示即使属性的类型是非可空类型,赋值时也允许null值。它告诉编译器在赋值过程中不产生警告。
using System;
using System.Diagnostics.CodeAnalysis;
public class MyClass
{
private string _innerValue = string.Empty;
[AllowNull]
public string MyValue
{
get
{
return _innerValue;
}
set
{
_innerValue = value ?? string.Empty;
}
}
}
public class Program
{
public static void Main()
{
// 创建MyClass类的一个实例
MyClass instance = new MyClass();
// 打印初始值(应该是空字符串)
Console.WriteLine($"Initial value: '{instance.MyValue}'"); // Output: ''
// 设置一个非null值
instance.MyValue = "Hello, World!";
Console.WriteLine($"After setting non-null value: '{instance.MyValue}'"); // Output: 'Hello, World!'
// 设置null值
instance.MyValue = null;
Console.WriteLine($"After setting null value: '{instance.MyValue}'"); // Output: ''
// 再次设置一个非null值
instance.MyValue = "Another Value";
Console.WriteLine($"After setting another non-null value: '{instance.MyValue}'"); // Output: 'Another Value'
}
}
如果注释AllowNull,编译器会提示如下
启用可空引用类型
项目级别
需要使用 C# 8 或更高版本,在”项目名.csproj“文件中,<Nullable>enable</Nullable>
,enable
代表启用
在.NET 6之前,该特性默认是禁用的,即为disable
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
文件方式
要控制每个文件,可以使用#nullable enable
开始启用,#nullable disable
开始禁用
class Program
{
static void Main()
{
Console.WriteLine("");
}
#nullable disable
static void A()
{
Version.TryParse("1.0.0", out var version);
Console.WriteLine(version.Major);
}
#nullable enable
static void B()
{
Version.TryParse("1.0.0", out var version);
Console.WriteLine(version.Major);
}
}
参考
- C# 8:可为 null 的引用类型 - Meziantou 的博客 — C# 8: Nullable Reference Types - Meziantou's blog
- C# 项目的 nullable 检查 - harrychinese - 博客园 (cnblogs.com)
- 可为 null 的引用类型 - C# |Microsoft学习 — Nullable reference types - C# | Microsoft Learn
- C# 8.0+新武器:如何用Nullable Reference Types消除99%的空指针异常? - 知乎 (zhihu.com)
- .NET 6新特性试用 | 可空引用类型 - 知乎 (zhihu.com)
- 了解可空性 - 培训 |Microsoft学习 — Understanding nullability - Training | Microsoft Learn
- 表达意图 - 培训 |Microsoft学习 — Expressing intent - Training | Microsoft Learn
- 试用可为 null 的引用类型 - .NET 博客 — Try out Nullable Reference Types - .NET Blog (microsoft.com)
- https://www.cnblogs.com/RainFate/p/17236969.html#_label0
- http://www.easy-dotnet.com/pages/72a0be/
- https://blog.csdn.net/longhaoyou/article/details/69257790
- https://blog.csdn.net/weixin_43267344/article/details/100034166
- https://blog.csdn.net/pmandy/article/details/83813327
- https://www.cnblogs.com/taofh/archive/2009/08/27/1554807.html