当用作泛型时,可空枚举类型不能分配给 null

Nullable enum type cannot be assigned to null when used as a generic

我正在使用 C# 9,我 运行 遇到了这个奇怪的问题,所以我在下面写了一个简单的例子来演示它。我需要将可为 null 的枚举的值设置为 null,但在通过泛型类型执行此操作时出现错误。如果我将枚举类型硬编码到 class 中,它工作正常,所以我不确定为什么在将相同类型用作泛型时它不起作用。似乎 TOption? 被视为 TOption 而忽略了可为空的部分,这将使该错误有意义,因为它会尝试将 null 分配给不可为空的值类型。

这是可空类型、值类型和编译器假设之间的 st运行ge 冲突吗?当 Option 用作通用类型时, TOption? 不应该与 Option? 完全相同吗?

请注意,在解决此问题的实际情况下,我无法将 TOption 约束为值类型,而且我认为此约束不是必需的。我不需要 TOption 成为值类型,我只需要该字段可以为空——无论它是 class 还是结构。

关于将 Option? 放入 TOption,我仍然需要将其视为不可空的字段。所以我不能这样做,我需要泛型中的实际类型,但我需要能够区分该类型的不可空和可空字段——独立于结构或 class 的类型。我应该指出,我使用的是可为空的引用类型,因此 classes 被视为不可为空,除非使用 ?.

指定
public class Program
{
    public static void Main(string[] args)
    {
        var test = new Test<Option>();
        test.Option1 = null;
        test.Option2 = null; // Cannot convert null to 'Program.Option' because it is a non-nullable value type
    }

    public enum Option { A, B, C }

    public class Test<TOption>
    {
        public Option Option0 { get; set; }
        public Option? Option1 { get; set; }
        public TOption? Option2 { get; set; }
    }
}

Nullable<T> 要求 T 是值类型(参见 documentation),但这不能由编译器从您的泛型类型派生。所以你需要帮助他使用约束:

public class Test<TOption> where TOption : struct
{
    public Option? Option1 { get; set; }
    public TOption? Option2 { get; set; }
}

编辑:正如您现在所说的,您不能依赖 TOption 作为值类型,您需要通过在泛型实例化而不是定义中指定 Nullable 来重构整个事物:

public class Program
{
    public static void Main(string[] args)
    {
        var test = new Test<Option?>();
        test.Option1 = null;
        test.Option2 = null;
    }

    public enum Option { A, B, C }

    public class Test<TOption>
    {
        public Option? Option1 { get; set; }
        public TOption Option2 { get; set; }
    }
}

在不限制 struct 或使用 default 的情况下最接近您想要的内容,它提供泛型参数中的可空类型(可能适合也可能不适合你)

var test = new Test<Option?>
{
    Option1 = null,
    Option2 = null // works
};

Console.WriteLine(test.Option2.HasValue);

问题在于,泛型 class 仍然不知道它在内部是受限于结构还是 class,这可能仍会以各种方式限制您,具体取决于用途案例在这里。

因此根据您更新后的要求,如果您不能使用可空类型;您需要一个可为空的通用实例 属性;并且您不能限制为结构,那么您可能需要重新考虑您的问题。 CLR 无法在编译时计算出您提供的泛型参数可以为空,因此会产生编译器错误。

为什么不是这样的?

   public class Program
    {
        public enum EnumOption { A, B, C }
        public class ClassOption { public int A { get; set; } }
        public interface InterfaceOption { public int A { get; set; } }
        public struct StructOption { int A; }

        public class Test<TOption>
        {
            public TOption GenericOption { get; set; }
        }
        

       // main entry
        public static void Main(string[] args)
        {
            // reference type  type 
            new Test<ClassOption>().GenericOption = null;
            new Test<InterfaceOption>().GenericOption = null;
            // nullable value type
            new Test<StructOption?>().GenericOption = null;
            new Test<EnumOption?>().GenericOption = null;
            new Test<int?>().GenericOption = null;

            // non nullable value type, use default for init/comparison
            new Test<StructOption?>().GenericOption = default;
            new Test<EnumOption?>().GenericOption = default;
            new Test<int?>().GenericOption = default;
        }

    }

这个失败的原因和我给的解释很相似。虽然在那种情况下有一个解决方法,但在您的情况下,您实际上 想要 可以为 null 的值类型,这使得这几乎不可能。

根据 T 是什么,

T? 对 CLR 意味着两种截然不同的事情。如果 T 是值类型,则表示 Nullable<T>。如果 T 是引用类型,就 CLR 而言,T? 实际上与 T(具有某些属性)相同。

那么编译器在编译你的代码的时候,会说Option2是什么类型的呢?换句话说,如果您使用反射检查 typeof(Test<>) 的成员(注意开放类型),那么 Option2 属性 的类型是什么?

在您的理想世界中,当 TOption 是值类型时,您希望 Option2Nullable<TOption> 类型,当 TOption 是引用类型。但是,如果我们要检查 typeof(Test<>) 的 属性 的类型,我们会得到哪种类型?不可能两者都,可以吗?

实际上,编译器选择 TOption 作为 Option2 的类型,并将 Option2 的类型视为可为 null,但也要记住它可能是非- 也可以为 null 的值类型。

这就是为什么仅通过 T?.

无法实现“可空值和引用类型”的原因

一个相当丑陋的解决方法是创建你自己的 Nullable<T>,它不限制 T 到值类型:

struct MyNullable<T> where T: notnull {
    private T value;
    
    public bool HasValue {
        get;
    }
    
    public T Value { 
        get {
            if (HasValue) {
                return value;
            } else {
                throw new InvalidOperationException("Value is not present!");
            }
        } 
    }
    
    public MyNullable(T t) {
        value = t;
        HasValue = t != null;
    }
}

现在你可以做:

public class Test<TOption>
{
    public Option Option0 { get; set; }
    public Option? Option1 { get; set; }
    public MyNullable<TOption> Option2 { get; set; }
}