My insight here is that it's usually not useful to distinguish between null
and empty strings. Sometimes it is, but usually not. Usually I like to assign my strings as empty if they would have been null.
var lastName = Request.Params["lastName"] ?? ""
Thus I had the idea of a struct
which would wrap a real string, internally coalesce it to ""
, prevent all manner of null references, provide "less dangerous" versions of common string operations, yet still be compatible with strings.
This is an incomplete listing--I just wanted to get feedback on the idea. Please let me know why this is awesome, mediocre, or just plain terrible!
//with normal string
public void WriteName(string name)
{
if(name != null && name.Length > 30)
{ name = name.Substring(0,30); }
Console.WriteLine(name);
}
//with safestring
public void WriteName(SafeString name)
{
Console.WriteLine(name.ReduceTo(30));
}
Below is the SafeString
struct:
/// <summary>
/// Struct wrapper for a string, to prevent null references when you
/// don't care to distinguish between `null` and empty strings.
/// Has implicit conversions to & from string
/// </summary>
public struct SafeString : IEquatable<SafeString>, IEquatable<string>
{
/*
The wrapped string value.
This will be properly initialized only if the constructor is called.
It's possible to create a struct with `default(SafeString)`
or by calling the parameterless ctor, `new SafeString()`
C#6 will allow us to define a parameterless ctor,
but there is still the issue of `default(SafeString)` which does not call any ctor.
Thus when we wish to use the value of `_theString`,
we need to either to check for null or funnel access through a helper
*/
private string _theString;
public SafeString(string s)
{
_theString = s ?? "";
}
public override string ToString()
{
//perform initialization in case this was created without using the ctor
return _theString ?? (_theString = "");
}
public int Length { get { return ToString().Length; } }
//safe substring method -- so you don't get an error when asking for a substring
// longer than the original
public SafeString SubString(int start, int length)
{
if (start < 0)
{
start = 0;
}
if (length <= 0)
{
return "";
}
var over = Math.Max((start + length) - Length, 0);
length = length - over;
return ToString().Substring(start, length);
}
//shorthand for SubString(0,length), for when you wish to cut a string down to size
public SafeString ReduceTo(int length)
{
if (length > Length)
{
return this;
}
return SubString(0, length);
}
public bool IsEmpty { get { return string.IsNullOrEmpty(_theString); } }
public bool IsWhitespace { get { return string.IsNullOrWhiteSpace(_theString); } }
public static implicit operator string(SafeString ss)
{
return ss.ToString();
}
public static implicit operator SafeString(string s)
{
return new SafeString(s);
}
public override int GetHashCode()
{
return ToString().GetHashCode();
}
public override bool Equals(object obj)
{
if (obj is SafeString)
{
return Equals((SafeString) obj);
}
if (obj is string)
{
return Equals((string) obj);
}
return false;
}
public bool Equals(SafeString other)
{
return ToString() == other.ToString();
}
public bool Equals(string other)
{
if (other == null)
{
return ToString() == ""; //we are never 'null'
}
return other == ToString();
}
}