如何正确地避免对 `default(T)` 的原始痴迷?

How to avoid primitive obsession correctly with handling of `default(T)`?

我想避免对追随 struct 的原始痴迷。我这样做有两个目标:

实施:

public struct SomeId : IEquatable<SomeId>
{
    public static readonly SomeId Empty = new SomeId(String.Empty);

    private SomeId(string someId)
    {
        Value = someId;
    }

    public string Value { get; }

    public static bool operator ==(SomeId left, SomeId right) => left.Equals(right);
    public static bool operator !=(SomeId left, SomeId right) => !(left == right);
    public static implicit operator string(SomeId id) => id.Value;
    public override int GetHashCode() => Value.GetHashCode();
    public override bool Equals(object obj) => Value.Equals(obj);
    public bool Equals(SomeId other) => String.Equals(Value, other.Value, StringComparison.Ordinal);

    public static SomeId? Create(string value)
    {
        if (String.IsNullOrWhiteSpace(value))
        {
            return new SomeId(value);
        }

        return null;
    }
}

代码很多,但还不够完美!事实上,它解决了主要问题,我不会到处传递字符串,而是为方法提供有意义的签名。

不过,我可以通过 default(SomeId) 创建新实例将无效值偷偷带入我的应用程序 - 因为 Valuestring 类型,我会得到 SomeId 处于无效状态 Value = null.

最好的解决方案是什么?我应该关心吗?

当然可以,例如:

private string _value;
public string Value => _value ?? String.Empty

...但是每当我访问这个 属性 时额外的空检查让我很烦。

你应该关心吗?是的,我想是这样。创建零初始化结构(default(SomeId)new SomeId()new SomeId[n])非常容易,并且您的结构在该状态下语义无效。

您有几个选择:

  • getter 中的空合并(您提出的解决方案)。你是对的,如果该字段为空,那总是会导致执行更多的指令。问题是,那些额外的指令(例如加载空值、比较相等性、加载静态字段)是否对执行速度有可衡量的影响?
  • 检查 getter 中的 null 并根据需要将字段设置为 string.Empty。从技术上讲,它是一个 getter 有副作用(即使数据被封装),有些人对此有强烈的意见,但你也可以称之为惰性初始化。
  • 声明默认实例无效,就像 ImmutableArray<T> 那样。