“可空引用类型的目标是防止空引用异常”
可空值类型与可空引用类型
在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 enablestring 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.34num = numNotNull ?? 5.34;Console.WriteLine("num的值:{0}", num); //num的值:3.14157Console.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 disablestatic void A(){Version.TryParse("1.0.0", out var version);Console.WriteLine(version.Major);}
#nullable enablestatic 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