【问题标题】:Nullable enum type cannot be assigned to null when used as a generic可空枚举类型在用作泛型时不能分配给空
【发布时间】:2021-01-19 08:09:54
【问题描述】:

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

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

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

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

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; }
    }
}

【问题讨论】:

  • 可空值和引用类型是非常不同的事物类型。你不能表达“无论类型是值还是引用类型都可以为空”
  • "我认为这个约束应该是不必要的" - 你的想法和规范所说的是两件不同的事情
  • 为了强调这一点,我建议您按照给出的答案中的建议修改Test(限制为struct),并创建一个限制为classTest2。编译两者,然后用 ILDASM 反编译。您会看到两者的代码不同
  • @Damien_The_Unbeliever 如果我想要的在当前编译器中是不可能的,那很好,我只想确定我想要的理论上应该是可能的。
  • @Cains 这就是为什么所有人都回答和评论来解决你的枚举问题,乍一看......因为标题、代码和暴露的问题。而且你已经告诉了每个人这不合适。吨的 cmets 意味着问题需要的细节或清晰度(我不能再次投票)。因此,我删除了我的答案并投票赞成@.00110001。所以请看一下xyproblem.info

标签: c# nullable


【解决方案1】:

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

T? 对 CLR 意味着两个非常不同的东西,具体取决于 T 是什么。如果T 是值类型,则表示Nullable&lt;T&gt;。如果T 是一个引用类型,那么就CLR 而言,T? 实际上与T 相同(带有一些属性)。

那么当编译器编译你的代码时,它会说Option2 是什么类型的?换句话说,如果您使用反射检查typeof(Test&lt;&gt;) 的成员(注意开放类型),Option2 属性的类型是什么?

在您的理想世界中,当TOption 是值类型时,您会希望Option2 的类型为Nullable&lt;TOption&gt;,而当TOption 是引用类型时,您希望TOption 的类型为TOption。但是如果我们要检查typeof(Test&lt;&gt;) 的属性类型,我们会得到哪种类型?不可能两者都,对吗?

实际上,编译器选择TOption 作为Option2 的类型,并将Option2 的类型视为可为空的,但请记住,它也可能是不可为空的值类型。

这就是为什么仅仅通过T? 无法实现“可空值和引用类型”的原因。

一个相当丑陋的解决方法是创建自己的Nullable&lt;T&gt;,它不会将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; }
}

【讨论】:

  • @.Sweeper 什么是notnull 以及此处的非空值如何为空:t != null?与@.00110001 答案相比,这如何解决 OP 问题?
  • 尽管 C# 语言中添加了所有新的可空性添加,但 CLR 似乎不会在理想世界中实现我想要的。我会看看您使用自定义可空类型的解决方法是否适用于我的情况,否则我只需要考虑到这个限制来重新设计。
  • @OlivierRogier 请参阅this list 中的解释。我把它放在那里只是为了防止像MyNullable&lt;int?&gt; 这样的嵌套空值。
  • @.Sweeper 我无法在 VS2017 中签入,但我不明白不可为空如何为空并解决 OP 问题。
  • @Cains 请注意,内置的Nullable 有很多语法糖和(取消)装箱优化,而您自己的Nullable 没有,这就是为什么这是一个“丑陋的" 解决方法。
【解决方案2】:

Nullable&lt;T&gt; 要求 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; }
    }
}

【讨论】:

  • 我在发帖后很快注意到了这一点,并做了一个说明来解决这个问题。 TOption 可能不是一个结构,我也不需要它,它恰好是这个例子中的一个。我需要的是可空性,而不是确定的值类型。
  • 如果 TOption 是引用类型,Nullable&lt;TOption&gt; 应该是什么意思?
  • 我想? 只是纯粹的语法糖吗?理想情况下,我希望如果它是引用类型,它只是类型,因为它已经可以为空。我在这里实际上要做的是使字段可以为空——引用类型已经是,值类型将包含在Nullable中。
  • 参见 docs.microsoft.com/en-us/dotnet/api/system.nullable-1: public struct Nullable&lt;T&gt; where T : struct: 所以 Nullable 仅适用于值类型。
  • 我在上面进行了编辑以解决您的新建议。我认为由于语言/编译器的限制,这可能是不可能的。这里的一个重要细节是我也在使用可为空的引用类型,所以我不确定? 是否一定是Nullable 的直接插入。
【解决方案3】:

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

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

Console.WriteLine(test.Option2.HasValue);

这样做的问题是,泛型类仍然不知道它是在内部被限制为结构还是类,这可能仍然会以各种方式限制您,具体取决于此处的用例。

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

【讨论】:

  • 不幸的是,这不适合我,因为我还需要该泛型的不可空字段,我在我的问题中添加了更多详细信息。
【解决方案4】:

为什么不这样呢?

   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;
        }

    }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-09-19
    • 2017-09-25
    • 2019-06-24
    • 1970-01-01
    相关资源
    最近更新 更多