这是我最近做一些关于依赖注入功能时候遇到的事情,顺便就写点东西记录一下。
可空类型
故事是这样的,首先是C#有一个可空值类型的概念:
int? i = null; i = 10;
这个概念很早就有了,没记错的话可能在C# 5或者更早的时候,所以我们就不展开说了。。然后最近十一月新出的C# 10 / .NET 6 出了个新东西,可空引用类型,大概这样:
string? str = null; str = "Hello World";
当然这个东西其实C# 8.x的时候就有了,只是从 .NET 6 开始,这个东西在新项目中默认开启了。(但C# 10相比8对这个东西有一些改进,本文以10版本说明)
不过这个新东西会让一些刚看到的开发者困惑,可空值类型好理解,原本int
、float
这类的值类型是没法直接设为null的,你就是不给初始值也会有个默认值0
. 但这个可空引用类型又是咋回事呢?引用类型本来就可以设为null
啊,为啥要多此一举呢?
这就得稍微介绍下这个新特性了。
可空引用类型
如果在 .NET 6 / C# 10 环境下新建一个项目,编写如下代码
string str = null; str = "Hello World"; Console.WriteLine(str);
(Console.WriteLine
是控制台输出,看我写东西的读者可能Unity开发者偏多,没见过这个,可以粗略的认为它类似于Unity的Debug.Log
)
这个在以前是很平常的代码,但在 .NET 6 / C# 10
环境下却会得到一个警告。

意思大概就是,新的项目默认都启用了“可空引用类型”这个特性,于是现在默认情况下,它是不允许我们给引用类型赋值为null
的。
那我们要让它可以被赋值成null
怎么办呢?加个问号:
string? str = null;
那么为什么新的C#要改成这样呢?简单来说,首先是降低代码在运行时引发空引用异常(System.NullReferenceException
)的可能性,它会在编写代码的时候就进行分析并对可能为null的地方提出警告。然后就是这一做法会让程序在运行时带来一些效率上的提高,这个就不展开说了。
然后这个新特性会直接导致我们写代码的习惯发生比较大的变化,一开始会很不适应,需要一些时间来改变思维方式。
首先是,所有我们代码可能会需要变成null的地方,都需要加问号.
//变量 string str? = null; var str2 = "hello"; //用var声明也会默认先推断成"string?"然后再在编译阶段进一步优化。 //方法 public void Foo(string? msg) => xxx;
刚换到新版本这种写法感觉最别扭的地方之一就是,写class的时候, 各种public的东西也得给默认值。
public class MeowDto { public string Name { get; set; } = string.Empty; //给默认值 } //或 public class MeowDto { public string? Name { get; set; } //声明可空 } //或 public class MeowDto { public MeowDto(string name) //构造函数赋值 { this.Name = name; } public string Name { get; set; } }
在一个接受不可空参数的方法中传入可空的值,也会出现警告:

于是对应的解决方法,可以在调用前加判空:

或者在传参的时候给变量后面加个!
符号,告诉编译器我觉得这儿没问题了,你别管它。

就像?.
语法糖一样,现在也有对应的!.
写法。
MeowDto? dto = Context.Payload?.Get<MeowDto>(); logger.Information(dto!.Name); //(上面具体内容是我瞎编的)
依赖注入
关于可空类型的介绍大概就是上面这些了,接下来我们说说依赖注入的问题。(下面的东西对于大部分开发者来说没啥用,您也可以关掉页面不往下看了。)
比如说我们有如下一个类需要实现依赖注入:
public class Meow { [Inject] public IMeowService? MeowService { get; set; } [Inject] public int? Num { get; set; } //属性没啥实际意义,瞎编的。 }
这里例子用的是属性注入,属性注入通常用途更多一些,不仅仅是服务容器的注入,比如Asp .Net Core注入请求参数啥的也都是类似的实现方法。当然,构造方法注入也是同一个道理。
这里我们顺便也可以看到,如果我们不想给初始值的话,就都用?
符号声明成可空的形式了。
然后如果要对这个类的实例进行属性注入的话,通常我们就使用反射了。
using System.Reflection; Meow meow = new Meow(); //随便通过什么方式得到一个实例 var properties = typeof(Meow).GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) .Where(p => p.IsDefined(typeof(InjectAttribute))).ToArray(); foreach (var property in properties) Console.WriteLine(property.Name);
得到属性信息之后,下一步我们就得知道它们的类型了。
那么它们分别是什么类型呢,首先看int?
可空值类型。

其实int?
就相当于是System.Nullable<int>
这么个类型:

那么,接下来我们要怎么通过反射为它赋值呢,像这样:
using System.Reflection; Meow meow = new Meow(); //随便通过什么方式得到一个实例 var properties = typeof(Meow).GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) .Where(p => p.IsDefined(typeof(InjectAttribute))).ToArray(); foreach (var property in properties) { if (property.PropertyType.IsValueType) //首先判断这是个值类型 { if(property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) //判读nullable值类型 { var propertyType = property.PropertyType.GetGenericArguments()[0]; //获取真实的值类型 if(propertyType == typeof(int)) //以下是简化写法,现实生活中没有这么干的 { property.SetValue(meow, 10); //赋值直接不用管nullable,直接赋值int就行 continue; } } } }
(注:C# 10 / .NET 6 默认有隐式using的功能,所以你看到的代码中只using了一个命名空间。)
然后,怎么反射给可空引用类型赋值呢?
其实,可空引用类型的PropertyType
和普通的类型没啥区别:
if (property.PropertyType == typeof(IMeowService)) //以下是简化写法,现实生活中没有这么干的 { property.SetValue(meow, new MeowService()); }
有些时候,我们可能需要明确需要知道这个引用类型到底是不是可空引用类型,比如Asp .Net Core 6在做模型验证的时候,会给不可空的类型搞一个类似[Required]
的效果。
那么,怎么做呢?
有两种方法,第一种是我在StackOverflow找到的,适用于C# 8以上版本.
if (property.PropertyType == typeof(IMeowService)) { bool b_nullable = IsNullable(property.PropertyType, property.DeclaringType, property.CustomAttributes); Console.WriteLine(b_nullable); continue; } static bool IsNullable(Type memberType, MemberInfo? declaringType, IEnumerable<CustomAttributeData> customAttributes) { if (memberType.IsValueType) return Nullable.GetUnderlyingType(memberType) != null; var nullable = customAttributes.FirstOrDefault(x => x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableAttribute"); if (nullable != null && nullable.ConstructorArguments.Count == 1) { var attributeArg = nullable.ConstructorArguments[0]; if (attributeArg.ArgumentType == typeof(byte[])) { var _args = (ReadOnlyCollection<CustomAttributeTypedArgument>)attributeArg.Value!; if (_args.Count > 0 && _args[0].ArgumentType == typeof(byte)) { return (byte)_args[0].Value! == 2; } } else if (attributeArg.ArgumentType == typeof(byte)) { return (byte)attributeArg.Value! == 2; } } for (var type = declaringType; type != null; type = type.DeclaringType) { var context = type.CustomAttributes.FirstOrDefault(x => x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableContextAttribute"); if (context != null && context.ConstructorArguments.Count == 1 && context.ConstructorArguments[0].ArgumentType == typeof(byte)) { return (byte)context.ConstructorArguments[0].Value! == 2; } } return false; }
以上代码参考的来源是这个:
但乍一看确实也云里雾里的,啥意思呢?
首先,如果声明了一个可空引用类型的属性或者字段的话,那么有可能在这个属性或字段上被编译器直接加了一个Attribute叫System.Runtime.CompilerServices.NullableAttribute
, 也有可能没有.
这个Attribute仅供编译器使用的,你在自己的代码里是不让用的,所以我们这儿是用字符串判断的。它大概长这样:
namespace System.Runtime.CompilerServices { [AttributeUsage( AttributeTargets.Class | AttributeTargets.Event | AttributeTargets.Field | AttributeTargets.GenericParameter | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = false, Inherited = false)] public sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte flag) { NullableFlags = new byte[] { flag }; } public NullableAttribute(byte[] flags) { NullableFlags = flags; } } }
我们代码里一开始就是在property上找它,这里面其中有用的东西就是NullableFlags
,它什么意思呢,在C#的编译器roslyn的GitHub页面上有这么个描述:
Each type reference in metadata may have an associated NullableAttribute with a byte[] where each byte represents nullability: 0 for oblivious, 1 for not annotated, and 2 for annotated.
所以我们上文代码在判断这个”annotated”,也就是2
.
然后我们所这个NullableAttribute
可能有可能没有,如果没有的话,我们就需要在它的声明类型上找另一个东西System.Runtime.CompilerServices.NullableContextAttribute
, 这也是一个仅供编译器使用的类,它长这样:
namespace System.Runtime.CompilerServices { [System.AttributeUsage( AttributeTargets.Class | AttributeTargets.Delegate | AttributeTargets.Interface | AttributeTargets.Method | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)] public sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte flag) { Flag = flag; } } }
比如在我们的案例中可以这么打印一下瞧瞧:

可以看到这两个Attribute都被放到我们的class上了,而property上其实是没有的。
更多相关细节可以参考:
然后对于 .NET 6以上版本的话,我们就不用管上面那堆东西了,有简化的方法,于是就成了这样:
using System.Reflection; Meow meow = new Meow(); //随便通过什么方式得到一个实例 var properties = typeof(Meow).GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) .Where(p => p.IsDefined(typeof(InjectAttribute))).ToArray(); NullabilityInfoContext nullabilityInfoContext = new NullabilityInfoContext(); foreach (var property in properties) { if (property.PropertyType.IsValueType) { //略 } if (property.PropertyType == typeof(IMeowService)) //简化写法,现实生活中别这么干 { var nullabilityInfo = nullabilityInfoContext.Create(property); if (nullabilityInfo.WriteState is NullabilityState.Nullable) Console.WriteLine("这是可空引用类型"); } }
暂无评论