Single

C#与可空类型与依赖注入

这是我最近做一些关于依赖注入功能时候遇到的事情,顺便就写点东西记录一下。

可空类型

故事是这样的,首先是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版本说明)

不过这个新东西会让一些刚看到的开发者困惑,可空值类型好理解,原本intfloat这类的值类型是没法直接设为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;
}

以上代码参考的来源是这个:

https://stackoverflow.com/questions/58453972/how-to-use-net-reflection-to-check-for-nullable-reference-type

但乍一看确实也云里雾里的,啥意思呢?

首先,如果声明了一个可空引用类型的属性或者字段的话,那么有可能在这个属性或字段上被编译器直接加了一个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("这是可空引用类型");
    }
}

相关的讨论在这儿:https://github.com/dotnet/runtime/issues/29723

暂无评论

发表评论