I've written a C# struct
in order to encapsulate the idea of a string
being neither null
nor white space.
I was basically tired of writing and unit testing checks like the following:
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;
}
}
The idea is creating a type, let's call it NonEmptyString
which is auto safe, so that I can use this type everywhere avoiding these annoying string checks.
I came up with the following (full source is available here):
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);
}
}
}
Unfortunately in C# it is not possible to hide or editing the default constructor of a struct
, so it is entirely possible to write the following code:
var myNonEmptyString = new NonEmptyString(); // default constructor is always available
string value = myNonEmptyString; // value is actually null
I thought of two ways of improving this type in order to handle this scenario:
- use a default value, such as
"N.A."
, for theValue
property. Doing so, even when an instance ofNonEmptyString
is created via the default constructor the wrapped string is actually a non empty string - add a private readonly field
isInitialized
, whose default value isfalse
, in order to track whether the right constructor has been called (the field is set totrue
only in the constructor overload having thestring
parameter). Doing so it is possible to add a check at the beginning of each type member, so that anInvalidOperationException
is raised each time the programmer creates an instance via the default constructor and tries to use it in his code.
Is there any other way to better handle the inevitable presence of the default constructor? What approach do you suggest?
For the ones asking themselves "why didn't he chose a class, in order to avoid this mess with the default constructor from the beginning", the reason for avoiding a class is simple: in C# (at least before C# 8) a reference type value is allowed to contain a null reference (and by default each reference type variable contains a null reference, unless properly initialized).
If NonEmptyString
were be defined as a class it would be useless, because each piece of code receiving an instance of NonEmptyString
would have to check whether the instance contains a null reference.
Instead, I would like to get a type giving the guarantee that each possible instance contains an actual string (that is a string other than null, the empty string and a sequence of spaces).
null
,string.Empty
and strings like" "
(I mean strings composed only of white spaces). The type models the idea of a string containing at least one character other than' '
. Intuitively these are the strings "having an actual value" \$\endgroup\$isInitialized
flag). The point with the default value is that whichever default value you chose, it probably doesn't make any sense for the programmer using the type. It's not meaningful and it's just a trick to avoid a null. \$\endgroup\$String<TValidator, TComparer>
and inherit it in a situation likeclass ProductName : String<NotEmptyValidator, IgnoreCaseComparer> {}
. I wish .NET had something like this ready to be used... \$\endgroup\$