【问题标题】:C# generic type constraint for everything nullable所有可为空的 C# 泛型类型约束
【发布时间】:2013-11-07 08:34:14
【问题描述】:

所以我有这门课:

public class Foo<T> where T : ???
{
    private T item;

    public bool IsNull()
    {
        return item == null;
    }

}

现在我正在寻找一种类型约束,它允许我将所有内容用作类型参数,可以是null。这意味着所有引用类型,以及所有 Nullable (T?) 类型:

Foo<String> ... = ...
Foo<int?> ... = ...

应该可以。

使用class 作为类型约束只允许我使用引用类型。

其他信息: 我正在编写一个管道和过滤器应用程序,并希望使用 null 引用作为传递到管道的最后一项,以便每个过滤器都可以很好地关闭,进行清理等......

【问题讨论】:

  • @Tim 不允许 Nullables
  • 此链接可能对您有所帮助:social.msdn.microsoft.com/Forums/en-US/…
  • 不可能直接这样做。也许你可以告诉我们更多关于你的场景?或者您可以使用IFoo&lt;T&gt; 作为工作类型并通过工厂方法创建实例?这可以发挥作用。
  • 我不确定您为什么想要或需要以这种方式限制某些东西。如果您的唯一意图是将“if x == null”转换为 if x.IsNull()”,这对于 99.99% 习惯于前一种语法的开发人员来说似乎毫无意义且不直观。编译器不会让您这样做“ if (int)x == null" 无论如何,所以你已经被覆盖了。

标签: c# generics nullable


【解决方案1】:

如果您愿意在 Foo 的构造函数中进行运行时检查而不是编译时检查,则可以检查该类型是否不是引用或可为空的类型,如果是则抛出异常。

我意识到只进行运行时检查可能是不可接受的,但以防万一:

public class Foo<T>
{
    private T item;

    public Foo()
    {
        var type = typeof(T);

        if (Nullable.GetUnderlyingType(type) != null)
            return;

        if (type.IsClass)
            return;

        throw new InvalidOperationException("Type is not nullable or reference type.");
    }

    public bool IsNull()
    {
        return item == null;
    }
}

然后下面的代码编译,但是最后一个(foo3)在构造函数中抛出了异常:

var foo1 = new Foo<int?>();
Console.WriteLine(foo1.IsNull());

var foo2 = new Foo<string>();
Console.WriteLine(foo2.IsNull());

var foo3= new Foo<int>();  // THROWS
Console.WriteLine(foo3.IsNull());

【讨论】:

  • 如果您要这样做,请确保在 static 构造函数中进行检查,否则您将减慢每个泛型实例的构建速度类(不必要)
  • @EamonNerbonne 您不应该从静态构造函数中引发异常:msdn.microsoft.com/en-us/library/bb386039.aspx
  • 指南不是绝对的。如果你想要这个检查,你将不得不权衡运行时检查的成本与静态构造函数中异常的笨拙。由于您确实在这里实现了一个穷人静态分析器,因此除了在开发期间,不应抛出此异常。最后,即使您想不惜一切代价避免静态构造异常(不明智),那么您仍然应该在实例构造函数中尽可能多地静态完成工作 - 例如通过设置标志“isBorked”或其他任何东西。
  • 顺便说一句,我认为您根本不应该尝试这样做。在大多数情况下,我宁愿将其视为 C# 限制,而不是尝试使用泄漏、容易失败的抽象。例如。一个不同的解决方案可能是只需要类,或者只需要结构(并明确地使 em 可以为空) - 或者两者都做并有两个版本。这不是对这个解决方案的批评。只是这个问题不能很好地解决——除非,也就是说,你愿意编写一个自定义的 roslyn 分析器。
  • 您可以两全其美 - 保留您在静态构造函数中设置的 static bool isValidType 字段,然后只需在实例构造函数中检查该标志,如果它是无效类型则抛出每次构建实例时都不会进行所有检查工作。我经常使用这种模式。
【解决方案2】:

我不知道如何在泛型中实现等价于 OR。但是我可以建议使用 default 关键字来为可空类型创建 null 并为结构创建 0 值:

public class Foo<T>
{
    private T item;

    public bool IsNullOrDefault()
    {
        return Equals(item, default(T));
    }
}

你也可以实现你的 Nullable 版本:

class MyNullable<T> where T : struct
{
    public T Value { get; set; }

    public static implicit operator T(MyNullable<T> value)
    {
        return value != null ? value.Value : default(T);
    }

    public static implicit operator MyNullable<T>(T value)
    {
        return new MyNullable<T> { Value = value };
    }
}

class Foo<T> where T : class
{
    public T Item { get; set; }

    public bool IsNull()
    {
        return Item == null;
    }
}

例子:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(new Foo<MyNullable<int>>().IsNull()); // true
        Console.WriteLine(new Foo<MyNullable<int>> {Item = 3}.IsNull()); // false
        Console.WriteLine(new Foo<object>().IsNull()); // true
        Console.WriteLine(new Foo<object> {Item = new object()}.IsNull()); // false

        var foo5 = new Foo<MyNullable<int>>();
        int integer = foo5.Item;
        Console.WriteLine(integer); // 0

        var foo6 = new Foo<MyNullable<double>>();
        double real = foo6.Item;
        Console.WriteLine(real); // 0

        var foo7 = new Foo<MyNullable<double>>();
        foo7.Item = null;
        Console.WriteLine(foo7.Item); // 0
        Console.WriteLine(foo7.IsNull()); // true
        foo7.Item = 3.5;
        Console.WriteLine(foo7.Item); // 3.5
        Console.WriteLine(foo7.IsNull()); // false

        // var foo5 = new Foo<int>(); // Not compile
    }
}

【讨论】:

  • 框架中原来的 Nullable 是一个结构体,而不是一个类。我认为创建一个模仿值类型的引用类型包装器不是一个好主意。
  • 第一个使用 default 的建议是完美的!现在,返回泛型类型的模板可以为对象返回 null,为内置类型返回默认值。
  • @CaseyAnderson 除非默认值有意义,在很多情况下确实如此。
【解决方案3】:

我遇到了这个问题,因为我想要一个可以接受任何“可为空”(引用类型或 Nullables)的通用静态方法的更简单的情况,这让我遇到了这个问题,但没有令人满意的解决方案。所以我想出了我自己的解决方案,它比 OP 提出的问题更容易解决,只需使用两个重载方法,一个采用 T 并具有约束 where T : class,另一个采用 T? 并具有where T : struct.

然后我意识到,该解决方案也可以应用于此问题,通过将构造函数设为私有(或受保护)并使用静态工厂方法来创建可在编译时检查的解决方案:

    //this class is to avoid having to supply generic type arguments 
    //to the static factory call (see CA1000)
    public static class Foo
    {
        public static Foo<TFoo> Create<TFoo>(TFoo value)
            where TFoo : class
        {
            return Foo<TFoo>.Create(value);
        }

        public static Foo<TFoo?> Create<TFoo>(TFoo? value)
            where TFoo : struct
        {
            return Foo<TFoo?>.Create(value);
        }
    }

    public class Foo<T>
    {
        private T item;

        private Foo(T value)
        {
            item = value;
        }

        public bool IsNull()
        {
            return item == null;
        }

        internal static Foo<TFoo> Create<TFoo>(TFoo value)
            where TFoo : class
        {
            return new Foo<TFoo>(value);
        }

        internal static Foo<TFoo?> Create<TFoo>(TFoo? value)
            where TFoo : struct
        {
            return new Foo<TFoo?>(value);
        }
    }

现在我们可以这样使用它:

        var foo1 = new Foo<int>(1); //does not compile
        var foo2 = Foo.Create(2); //does not compile
        var foo3 = Foo.Create(""); //compiles
        var foo4 = Foo.Create(new object()); //compiles
        var foo5 = Foo.Create((int?)5); //compiles

如果你想要一个无参数的构造函数,你不会得到重载的好处,但你仍然可以这样做:

    public static class Foo
    {
        public static Foo<TFoo> Create<TFoo>()
            where TFoo : class
        {
            return Foo<TFoo>.Create<TFoo>();
        }

        public static Foo<TFoo?> CreateNullable<TFoo>()
            where TFoo : struct
        {
            return Foo<TFoo?>.CreateNullable<TFoo>();
        }
    }

    public class Foo<T>
    {
        private T item;

        private Foo()
        {
        }

        public bool IsNull()
        {
            return item == null;
        }

        internal static Foo<TFoo> Create<TFoo>()
            where TFoo : class
        {
            return new Foo<TFoo>();
        }

        internal static Foo<TFoo?> CreateNullable<TFoo>()
            where TFoo : struct
        {
            return new Foo<TFoo?>();
        }
    }

并像这样使用它:

        var foo1 = new Foo<int>(); //does not compile
        var foo2 = Foo.Create<int>(); //does not compile
        var foo3 = Foo.Create<string>(); //compiles
        var foo4 = Foo.Create<object>(); //compiles
        var foo5 = Foo.CreateNullable<int>(); //compiles

这个解决方案有几个缺点,一个是你可能更喜欢使用'new'来构造对象。另一个是您将无法使用Foo&lt;T&gt; 作为类型约束的泛型类型参数:where TFoo: new()。最后是您需要的一些额外代码,尤其是在您需要多个重载构造函数时。

【讨论】:

    【解决方案4】:

    如前所述,您不能对其进行编译时检查。 .NET 中的通用约束严重缺乏,并且不支持大多数场景。

    但是我认为这是运行时检查的更好解决方案。它可以在 JIT 编译时进行优化,因为它们都是常量。

    public class SomeClass<T>
    {
        public SomeClass()
        {
            // JIT-compile time check, so it doesn't even have to evaluate.
            if (default(T) != null)
                throw new InvalidOperationException("SomeClass<T> requires T to be a nullable type.");
    
            T variable;
            // This still won't compile
            // variable = null;
            // but because you know it's a nullable type, this works just fine
            variable = default(T);
        }
    }
    

    【讨论】:

      【解决方案5】:

      这样的类型约束是不可能的。根据documentation of type constraints,没有同时捕获可空类型和引用类型的约束。由于约束只能组合在一起,所以无法通过组合来创建这样的约束。

      但是,您可以根据需要回退到无约束类型参数,因为您始终可以检查 == null。如果类型是值类型,则检查将始终评估为假。然后,您可能会收到 R# 警告“可能将值类型与 null 进行比较”,这并不重要,只要语义适合您。

      另一种方法是使用

      object.Equals(value, default(T))
      

      而不是空检查,因为 default(T) where T : class 始终为空。但是,这意味着您无法区分一个不可为空的值从未被明确设置或只是设置为其默认值的天气。

      【讨论】:

      • 我认为问题在于如何检查从未设置过的值。与 null 不同似乎表明该值已被初始化。
      • 这不会使方法无效,因为总是设置值类型(至少隐式设置为它们各自的默认值)。
      【解决方案6】:

      我用

      public class Foo<T> where T: struct
      {
          private T? item;
      }
      

      【讨论】:

        【解决方案7】:

        如果您只想允许可空值类型和引用类型,而不允许不可空值类型,那么我认为您从 C# 9 开始就不走运了。

        我正在编写一个管道和过滤器应用程序,并希望使用空引用作为传递到管道的最后一项,以便每个过滤器都可以很好地关闭,进行清理等......

        换句话说,您需要保留一个表示流结束的特殊值。

        考虑创建一个提供此功能的包装器类型。它类似于Nullable&lt;T&gt; 的实现方式,并且具有允许传输非流式null 值的额外好处,如果这有用的话。

        public readonly struct StreamValue<T>
        {
            public bool IsEndOfStream { get; }
            public T Value { get; }
        }
        

        【讨论】:

          【解决方案8】:
              public class Foo<T>
              {
                  private T item;
          
                  public Foo(T item)
                  {
                      this.item = item;
                  }
          
                  public bool IsNull()
                  {
                      return object.Equals(item, null);
                  }
              }
          
              var fooStruct = new Foo<int?>(3);
                  var b = fooStruct.IsNull();
          
                  var fooStruct1 = new Foo<int>(3);
                  b = fooStruct1.IsNull();
          
                  var fooStruct2 = new Foo<int?>(null);
                  b = fooStruct2.IsNull();
          
                  var fooStruct3 = new Foo<string>("qqq");
                  b = fooStruct3.IsNull();
          
                  var fooStruct4 = new Foo<string>(null);
                  b = fooStruct4.IsNull();
          

          【讨论】:

          • 这种类型允许 new Foo(42) 和 IsNull() 将返回 false,虽然语义正确,但并不是特别有意义。
          • 42 是“生命、宇宙和一切终极问题的答案”。简单地说:每个 int 值的 IsNull 都会返回 false(即使是 0 值)。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-01-21
          • 1970-01-01
          相关资源
          最近更新 更多