【问题标题】:Is there any way to combine these almost identical classes into one?有没有办法将这些几乎相同的类组合成一个?
【发布时间】:2013-12-15 17:37:23
【问题描述】:

跟进这个问题:Why is Nullable<T> considered a struct and not a class?

我有两个类,它们本质上是使用内部对象维护一些用户提供的值的元组。

当用户提供的值的类型是原始类型时,我必须将其包装在 Nullable&lt;T&gt; 中,以便它可以在元组中采用空值。

public class BundledClass<T> where T : class
{
    private Tuple<T, object> _bundle;
    public T Value
    { 
        get { return _bundle == null ? null : _bundle.Item1; }
        set { _bundle = new Tuple<T, object>(value, internalObj); }
    }
    //...

public class BundledPrimitive<T> where T : struct
{
    private Tuple<T?, object> _bundle;
    public T? Value
    { 
        get { return _bundle == null ? null : _bundle.Item1; }
        set { _bundle = new Tuple<T?, object>(value, internalObj); }
    }
    //...

如果我可以使用可以将基元或类作为类型参数的单个类来执行此操作,我会更喜欢它,但我看不到任何解决方法。并非没有想出某种自定义的Nullable 类,它可以装箱任何类型(不仅仅是类型where T:struct),以确保始终可以将Value 分配为null;

看来我至少应该能够将后一个类定义为

public class BundledPrimitive<T> : BundledClass<T?> { }

但即使这样也失败了,因为 Nullable&lt;T&gt; 不符合 : class 约束(根据链接的问题)。

【问题讨论】:

  • 如果我是这个类的用户,如何区分 Value 未设置和我在程序的其他地方将 Value 设置为 null?
  • @mikez 如果从未设置过该值,则使用内部_bundle = null。如果Value 设置为空,则内部_bundle 将是Tuple&lt;T, object&gt;(null, internalObj)。这并不重要,但用户可以使用返回 _bundle 的公共 get-only 属性看到这一点。
  • 是的,这将是内部状态的差异,但作为调用者,我无权访问_bundle。
  • @Alain BTW,我在答案中选择了名称BundledStruct 而不是BundledPrimitive,因为两者并不完全相同。 DateTime 是结构体但不是原语,string 是 C# 中的原语(虽然 Type.IsPrimitive 会返回 false),但它不是结构体。

标签: c# generics nullable type-constraints


【解决方案1】:

如果你只是这样设计你的类:

public abstract class Bundled<T>
{
    private Tuple<T, object> _bundle;
    public T Value
    { 
        get { return _bundle == null ? default(T) : _bundle.Item1; }
        set { _bundle = new Tuple<T, object>(value, internalObj); }
    }
}

然后可以通过将类型参数指定为Nullable&lt;T&gt; 来将其与结构一起使用,例如:

Bundled<string> foo; // handles classes
Bundled<float?> bar; // handles structs

这里唯一的问题是,用户可能会将此类与不可为空的结构一起使用,例如Bundled&lt;int&gt;。如果这确实是您的应用程序中的一个问题,您可以像这样声明更具体的子类型:

public class BundledClass<T> : Bundled<T> where T : class { }
public class BundledStruct<T> : Bundled<T?> where T : struct { }

您还可以将Bundled&lt;T&gt; 的构造函数设为内部,这样就不能从您的程序集外部调用它。这将确保用户不会创建自定义子类型来绕过您的 BundledClass / BundledStruct 包装器。

【讨论】:

  • 不能使用default(T)。如果用户创建了Bundled&lt;double&gt; foo,我绝对必须区分他们何时设置foo.Value = 0 与何时取消设置foo.Value(因此_bundle = Tuple&lt;T?, object&gt;(null, internalObj))。
  • 我想这和下面的答案一样有效,只要没有人试图在没有类型约束的情况下实现Bundled&lt;T&gt;。
  • @Alain 正如我所说,您可以将构造函数设为内部,因此只能从您的程序集中调用它。这意味着从Bundled 派生的任何外部类型都不会编译(至少在没有大量反射的情况下不会编译)。如果您希望用户能够从它们继承,请确保 BundledClass / BundledStruct 提供公共构造函数。
  • 这可以与静态构造函数中的运行时检查相结合,以确保T 是“好”的。这是因为没有编译时通用约束。静态构造函数应该去static Bundled() { if (typeof(T).IsValueType &amp;&amp; Nullable.GetUnderlyingType(typeof(T)) == null) throw new InvalidOperationException("T must allow null."); }
  • 您可以将此方法与@Alain's 结合使用,并使Bundled&lt;&gt; 抽象,而无需添加任何虚拟方法。这不会阻止同一个程序集中的代码从它派生,但它不会在同一个程序集中按原样使用,并且对于任何其他程序集,它都是禁止使用的。
【解决方案2】:

我现在能想到的最好的办法。它仍然是两个类,但它使我免于重复数百行代码:

public abstract class Bundled<T>
{
    protected Tuple<T, object> _bundle;
    public abstract T Value { get; set; }

    //... Everything else
}

public class BundledClass<T> : Bundled<T> where T : class
{
    public sealed override T Value
    { 
        get { return _bundle == null ? null : _bundle.Item1; }
        set { _bundle = new Tuple<T, object>(value, internalObj); }
    }
}

public class BundledPrimitive<T> : Bundled<T?> where T : struct
{        
    public sealed override T? Value
    { 
        get { return _bundle == null ? null : _bundle.Item1; }
        set { _bundle = new Tuple<T?, object>(value, internalObj); }
    }
}

【讨论】:

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