Как создать тип, представляющий строку, не равную нулю или пробелу - PullRequest
1 голос
/ 18 сентября 2019

Много раз мы передаем строки в конструктор бизнес-объектов, и мы хотим быть уверены, что эти строки действительно несут в себе значение.В таком сценарии мы выполняем проверку параметров конструктора и выдаем ArgumentException каждый раз, когда переданная строка является нулевым или пробелом.

Это пример того, что я имею в виду:

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

Усталый от повторения себя, я решил разработать автоматический сейф, представляющий собой строку, не равную нулю или пробелу.Таким образом, я могу напрямую использовать экземпляры этого типа в своем бизнес-коде и избежать какой-либо проверки, потому что каждый экземпляр типа автоматически безопасен (другими словами, код проверки теперь находится в одном месте, код для самого типа).

Это структура 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);
    }
  }
}

Используя мой новый тип, я могу изменить предыдущий код следующим образом:

public class Person 
{
  public NonEmptyString Name { get; }

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

Единственная проблема с этим дизайном представлена ​​конструктором по умолчанию , который всегда доступен, потому что мой тип - struct.

Если кто-то, использующий мой код, пишетvar myString = new NonEmptyString(); он получает экземпляр типа, который инкапсулирует ссылку null: это то, чего я хотел бы избежать, потому что при этом вся цель моего авто-безопасного типа становится недействительной.Другими словами, я не хочу полагаться на то, что программист не вызывает конструктор по умолчанию, я хотел бы сделать невозможным неправильное использование этого типа.

Я пришел с парой идей:

  • предоставляет значение по умолчанию для свойства только для чтения Value, что-то вроде "NA".Таким образом, даже когда вызывается конструктор по умолчанию, полученный экземпляр инкапсулирует ненулевое и непустое значение.

  • добавление флага, указывающего, был ли инициализирован тип, имеющего значение по умолчанию:false.Это состояние доступно только для чтения, и оно изменяется на true только при перегрузке конструктора, получающей строковый параметр.Таким образом, защита может быть добавлена ​​к любому члену типа, так что InvalidOperationException может быть вызвано каждый раз, когда программист пытается использовать неинициализированный экземпляр типа (то есть экземпляр типа, полученный путем вызоваконструктор по умолчанию).

У вас есть предложения?Какой подход вы предпочитаете?

Ответы [ 2 ]

0 голосов
/ 19 сентября 2019

Я не уверен, хорошо ли я понимаю ваш вопрос, но я предполагаю, что из

Идея предоставления бессмысленного значения по умолчанию просто для того, чтобы избежать нулевой ссылки, - хитрость.Вероятно, бросать во время выполнения, это лучше.Это тот же подход, который Microsoft использовал с Nullable.в этом случае вы можете получить доступ к значению экземпляра, даже если само значение отсутствует

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

Это, вероятно, может вам помочь.Код не проверен, он просто дает вам представление о том, что вы можете искать.

0 голосов
/ 18 сентября 2019

Вместо использования свойства auto вы можете использовать вспомогательное поле _value и реализовать вместо него getter для свойства Value.как

public string Value => _value ?? "";

Затем заставьте каждую функцию работать, когда _value равен нулю;

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...