简体   繁体   English

如何设计表示既不是 null 也不是空格的字符串的类型

[英]How to design a type representing a string neither null nor white space

Many times we pass strings in the constructor of business entities and we want to be sure that those strings actually carry a value with them.很多时候,我们在业务实体的构造函数中传递字符串,我们希望确保这些字符串实际上带有一个值。 In such a scenario we perform a validation of the constructor parameters and we throw an ArgumentException each time the passed in string is null or white space.在这种情况下,我们对构造函数参数进行验证,每次传入的字符串为 null 或空格时,我们都会抛出ArgumentException

This is an example of what I mean:这是我的意思的一个例子:

public class Person 
{
   public string Name { get; }

   public Person(string name)
   {
      if (string.IsNullOrWhiteSpace(name))
      {
         throw new ArgumentException("A person name cannot be null or white space", nameof(name));
      }

      this.Name = name;
   }
}

Tired of repeting myself I decided to design an auto safe type representing a string neither null nor white space.厌倦了重复自己,我决定设计一个自动安全类型,代表一个既不是 null 也不是空格的字符串。 This way I can directly use instances of that type in my business code and avoid any validation, because each instance of the type is auto safe (in other words the validation code is now in one place, the code for the type itself).这样我可以直接在我的业务代码中使用该类型的实例并避免任何验证,因为该类型的每个实例都是自动安全的(换句话说,验证代码现在在一个地方,即类型本身的代码)。

This is the NonEmptyString struct (original code is here ):这是NonEmptyString结构(原始代码在这里):

using System;

namespace Deltatre.Utils.Types
{
  /// <summary>
  /// This type wraps a string which is guaranteed to be neither null nor white space
  /// </summary>
  public struct NonEmptyString
  {
    /// <summary>
    /// Implicit conversion from <see cref="NonEmptyString"/> to <see cref="string"/>
    /// </summary>
    /// <param name="nonEmptyString">The instance of <see cref="NonEmptyString"/> to be converted</param>
    public static implicit operator string(NonEmptyString nonEmptyString)
    {
      return nonEmptyString.Value;
    }

    /// <summary>
    /// Explicit conversion from <see cref="string"/> to <see cref="NonEmptyString"/>
    /// </summary>
    /// <param name="value">The instance of <see cref="string"/> to be converted</param>
    /// <exception cref="InvalidCastException">Throws <see cref="InvalidCastException"/> when <paramref name="value"/> is null or white space</exception>
    public static explicit operator NonEmptyString(string value)
    {
      try
      {
        return new NonEmptyString(value);
      }
      catch (ArgumentException ex)
      {
        throw new InvalidCastException($"Unable to convert the provided string to {typeof(NonEmptyString).Name}", ex);
      }
    }

    /// <summary>
    /// Creates new instance of <see cref="NonEmptyString"/>
    /// </summary>
    /// <param name="value">The string to be wrapped</param>
    /// <exception cref="ArgumentException">Throws <see cref="ArgumentException"/> when parameter <paramref name="value"/> is null or white space</exception>
    public NonEmptyString(string value)
    {
      if (string.IsNullOrWhiteSpace(value))
        throw new ArgumentException($"Parameter {nameof(value)} cannot be null or white space", nameof(value));

      this.Value = value;
    }

    /// <summary>
    /// Gets the wrapped string
    /// </summary>
    public string Value { get; }

    /// <summary>Indicates whether this instance and a specified object are equal.</summary>
    /// <param name="obj">The object to compare with the current instance. </param>
    /// <returns>
    ///     <see langword="true" /> if <paramref name="obj" /> and this instance are the same type and represent the same value; otherwise, <see langword="false" />. </returns>
    public override bool Equals(object obj)
    {
      if (!(obj is NonEmptyString))
      {
        return false;
      }

      var other = (NonEmptyString)obj;
      return this.Value == other.Value;
    }

    /// <summary>Returns the hash code for this instance.</summary>
    /// <returns>A 32-bit signed integer that is the hash code for this instance.</returns>
    public override int GetHashCode()
    {
      unchecked
      {
        int hash = 17;
        hash = (hash * 23) + (this.Value == null ? 0 : this.Value.GetHashCode());
        return hash;
      }
    }

    /// <summary>
    /// Compares two instances of <see cref="NonEmptyString"/> for equality
    /// </summary>
    /// <param name="left">An instance of <see cref="NonEmptyString"/></param>
    /// <param name="right">An instance of <see cref="NonEmptyString"/></param>
    /// <returns></returns>
    public static bool operator ==(NonEmptyString left, NonEmptyString right)
    {
      return left.Equals(right);
    }

    /// <summary>
    /// Compares two instances of <see cref="NonEmptyString"/> for inequality
    /// </summary>
    /// <param name="left">An instance of <see cref="NonEmptyString"/></param>
    /// <param name="right">An instance of <see cref="NonEmptyString"/></param>
    /// <returns></returns>
    public static bool operator !=(NonEmptyString left, NonEmptyString right)
    {
      return !(left == right);
    }
  }
}

Using my new type I can change the previous code this way:使用我的新类型,我可以通过这种方式更改以前的代码:

public class Person 
{
  public NonEmptyString Name { get; }

  public Person(NonEmptyString name)
  {          
    this.Name = name;
  }
}

The only issue with this design is represented by the default constructor which is always available because my type is a struct .这种设计的唯一问题是默认构造函数,它始终可用,因为我的类型是struct

If anyone using my code writes var myString = new NonEmptyString();如果有人使用我的代码写var myString = new NonEmptyString(); he gets an instance of the type which encapsulates a null reference: this is something I would like to avoid because doing so the entire purpose of my auto safe type is invalidated.他得到一个封装了null引用的类型的实例:这是我想避免的,因为这样做会使我的自动安全类型的整个目的无效。 In other words I don't want to rely on the programmer not calling the default constructor, I would like to make it impossible to misuse this type.换句话说,我不想依赖程序员不调用默认构造函数,我想让它不可能误用这种类型。

I came up with a couple of ideas:我想出了几个想法:

  • provide a default value for the read only property Value , something such as "NA".为只读属性Value提供默认值,例如“NA”。 This way even when the default constructor is called the instance obtained encapsulates a non null and non white space value.这样,即使调用默认构造函数,获得的实例也封装了一个非 null 和非空白值。

  • adding a flag indicating whether the type has been initialized, having a default value of false .添加指示类型是否已初始化的标志,默认值为false This state is read only and it is changed to true only in the constructor overload receiving a string parameter.此 state 是只读的,仅在接收字符串参数的构造函数重载时才更改为true This way a guard can be added to any member of the type, so that an InvalidOperationException can be raised each time a programmer tries to use a non initialized instance of the type (that is, an instance of the type obtained by calling the default constructor).这样可以为该类型的任何成员添加一个保护,这样每次程序员尝试使用该类型的未初始化实例(即调用默认构造函数获得的该类型的实例)时,都会引发InvalidOperationException )。

Do you have any suggestion?你有什么建议吗? What approach do you prefer?你更喜欢什么方法?

Instead of using auto property, you can use backing field _value and implement getter for Value property instead.您可以使用支持字段_value并为Value属性实现 getter,而不是使用 auto 属性。 like喜欢

public string Value => _value ?? "";

Then make every function works when _value is null;然后让每个 function 在_value为 null 时工作;

I'm not sure if I understand your question well but I assume from我不确定我是否理解你的问题,但我假设

The idea of providing a meaningless default value just to avoid a null reference is a trick.提供无意义的默认值只是为了避免 null 引用的想法是一个技巧。 Probably throwing at runtime it's better.可能在运行时抛出它会更好。 That's the same approach Microsoft has used with Nullable.这与 Microsoft 对 Nullable 使用的方法相同。 in that case you are free to access the Value of an instance even when the value itself is missing在这种情况下,即使值本身丢失,您也可以自由访问实例的值

public class Person 
{
    private string _name;
    public string Name
    {
        get { return _name; }
        set
        {
            if(String.IsNullOrWhiteSpace(value) || String.IsNullOrEmpty(value))
            {
                value = "NA";
            }
            _name = value;
        }
    }

   public Person(string name)
   {
      this.Name = name;
   }
}

This probably might help you.这可能会对您有所帮助。 The code is untested, It is just to give you a glimpse of what you might be looking for.该代码未经测试,只是为了让您了解您可能正在寻找的内容。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM