C# creating a non-nullable string. Is it possible? Somehow?
You could of course have the following nString
struct:
public struct nString
{
public nString(string value)
: this()
{
Value = value ?? "N/A";
}
public string Value
{
get;
private set;
}
public static implicit operator nString(string value)
{
return new nString(value);
}
public static implicit operator string(nString value)
{
return value.Value;
}
}
...
public nString val
{
get;
set;
}
obj.val = null;
string x = obj.val; // <-- x will become "N/A";
This would allow casting from and to string
. Under the hood it performs the same cast as your example, you just don't have to type it out for every property. I do wonder what this does to maintainability for your application though.
In order to make my nString struct fully functional, I added every single string method to it including overloads. If anyone runs into this problem, feel free to copy paste this code and go nuts. I'll probably add the documentation to it next.
/// <summary>
/// Non-nullable string.
/// </summary>
public struct nString
{
public nString(string value)
: this()
{
Value = value ?? "";
}
public nString(char[] value)
{
Value = new string(value) ?? "";
}
public nString(char c, int count)
{
Value = new string(c, count) ?? "";
}
public nString(char[] value, int startIndex, int length)
{
Value = new string(value, startIndex, length) ?? "";
}
public string Value
{
get;
private set;
}
public static implicit operator nString(string value)
{
return new nString(value);
}
public static implicit operator string(nString value)
{
return value.Value ?? "";
}
public int CompareTo(string strB)
{
Value = Value ?? "";
return Value.CompareTo(strB);
}
public bool Contains(string value)
{
Value = Value ?? "";
return Value.Contains(value);
}
public void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count)
{
Value = Value ?? "";
Value.CopyTo(sourceIndex, destination, destinationIndex, count);
}
public bool EndsWith(string value)
{
Value = Value ?? "";
return Value.EndsWith(value);
}
public bool EndsWith(string value, StringComparison comparisonType)
{
Value = Value ?? "";
return Value.EndsWith(value, comparisonType);
}
public override bool Equals(object obj)
{
Value = Value ?? "";
return Value.Equals(obj);
}
public bool Equals(string value)
{
Value = Value ?? "";
return Value.Equals(value);
}
public bool Equals(string value, StringComparison comparisonType)
{
Value = Value ?? "";
return Value.Equals(value, comparisonType);
}
public override int GetHashCode()
{
Value = Value ?? "";
return Value.GetHashCode();
}
public new Type GetType()
{
return typeof(string);
}
public int IndexOf(char value)
{
Value = Value ?? "";
return Value.IndexOf(value);
}
public int IndexOf(string value)
{
Value = Value ?? "";
return Value.IndexOf(value);
}
public int IndexOf(char value, int startIndex)
{
Value = Value ?? "";
return Value.IndexOf(value, startIndex);
}
public int IndexOf(string value, int startIndex)
{
Value = Value ?? "";
return Value.IndexOf(value, startIndex);
}
public int IndexOf(string value, StringComparison comparisonType)
{
Value = Value ?? "";
return Value.IndexOf(value, comparisonType);
}
public int IndexOf(char value, int startIndex, int count)
{
Value = Value ?? "";
return Value.IndexOf(value, startIndex, count);
}
public int IndexOf(string value, int startIndex, int count)
{
Value = Value ?? "";
return Value.IndexOf(value, startIndex, count);
}
public int IndexOf(string value, int startIndex, StringComparison comparisonType)
{
Value = Value ?? "";
return Value.IndexOf(value, startIndex, comparisonType);
}
public int IndexOf(string value, int startIndex, int count, StringComparison comparisonType)
{
Value = Value ?? "";
return Value.IndexOf(value, startIndex, count, comparisonType);
}
public int IndexOfAny(char[] anyOf)
{
Value = Value ?? "";
return Value.IndexOfAny(anyOf);
}
public int IndexOfAny(char[] anyOf, int startIndex)
{
Value = Value ?? "";
return Value.IndexOfAny(anyOf, startIndex);
}
public int IndexOfAny(char[] anyOf, int startIndex, int count)
{
Value = Value ?? "";
return Value.IndexOfAny(anyOf, startIndex, count);
}
public string Insert(int startIndex, string value)
{
Value = Value ?? "";
return Value.Insert(startIndex, value);
}
public int LastIndexOf(char value)
{
Value = Value ?? "";
return Value.LastIndexOf(value);
}
public int LastIndexOf(string value)
{
Value = Value ?? "";
return Value.LastIndexOf(value);
}
public int LastIndexOf(char value, int startIndex)
{
Value = Value ?? "";
return Value.LastIndexOf(value, startIndex);
}
public int LastIndexOf(string value, int startIndex)
{
Value = Value ?? "";
return Value.LastIndexOf(value, startIndex);
}
public int LastIndexOf(string value, StringComparison comparisonType)
{
Value = Value ?? "";
return Value.LastIndexOf(value, comparisonType);
}
public int LastIndexOf(char value, int startIndex, int count)
{
Value = Value ?? "";
return Value.LastIndexOf(value, startIndex, count);
}
public int LastIndexOf(string value, int startIndex, int count)
{
Value = Value ?? "";
return Value.LastIndexOf(value, startIndex, count);
}
public int LastIndexOf(string value, int startIndex, StringComparison comparisonType)
{
Value = Value ?? "";
return Value.LastIndexOf(value, startIndex, comparisonType);
}
public int LastIndexOf(string value, int startIndex, int count, StringComparison comparisonType)
{
Value = Value ?? "";
return Value.LastIndexOf(value, startIndex, count, comparisonType);
}
public int LastIndexOfAny(char[] anyOf)
{
Value = Value ?? "";
return Value.LastIndexOfAny(anyOf);
}
public int LastIndexOfAny(char[] anyOf, int startIndex)
{
Value = Value ?? "";
return Value.LastIndexOfAny(anyOf, startIndex);
}
public int LastIndexOfAny(char[] anyOf, int startIndex, int count)
{
Value = Value ?? "";
return Value.LastIndexOfAny(anyOf, startIndex, count);
}
public int Length
{
get
{
Value = Value ?? "";
return Value.Length;
}
}
public string PadLeft(int totalWidth)
{
Value = Value ?? "";
return Value.PadLeft(totalWidth);
}
public string PadLeft(int totalWidth, char paddingChar)
{
Value = Value ?? "";
return Value.PadLeft(totalWidth, paddingChar);
}
public string PadRight(int totalWidth)
{
Value = Value ?? "";
return Value.PadRight(totalWidth);
}
public string PadRight(int totalWidth, char paddingChar)
{
Value = Value ?? "";
return Value.PadRight(totalWidth, paddingChar);
}
public string Remove(int startIndex)
{
Value = Value ?? "";
return Value.Remove(startIndex);
}
public string Remove(int startIndex, int count)
{
Value = Value ?? "";
return Value.Remove(startIndex, count);
}
public string Replace(char oldChar, char newChar)
{
Value = Value ?? "";
return Value.Replace(oldChar, newChar);
}
public string Replace(string oldValue, string newValue)
{
Value = Value ?? "";
return Value.Replace(oldValue, newValue);
}
public string[] Split(params char[] separator)
{
Value = Value ?? "";
return Value.Split(separator);
}
public string[] Split(char[] separator, StringSplitOptions options)
{
Value = Value ?? "";
return Value.Split(separator, options);
}
public string[] Split(string[] separator, StringSplitOptions options)
{
Value = Value ?? "";
return Value.Split(separator, options);
}
public bool StartsWith(string value)
{
Value = Value ?? "";
return Value.StartsWith(value);
}
public bool StartsWith(string value, StringComparison comparisonType)
{
Value = Value ?? "";
return Value.StartsWith(value, comparisonType);
}
public string Substring(int startIndex)
{
Value = Value ?? "";
return Value.Substring(startIndex);
}
public string Substring(int startIndex, int length)
{
Value = Value ?? "";
return Value.Substring(startIndex, length);
}
public char[] ToCharArray()
{
Value = Value ?? "";
return Value.ToCharArray();
}
public string ToLower()
{
Value = Value ?? "";
return Value.ToLower();
}
public string ToLowerInvariant()
{
Value = Value ?? "";
return Value.ToLowerInvariant();
}
public override string ToString()
{
Value = Value ?? "";
return Value.ToString();
}
public string ToUpper()
{
Value = Value ?? "";
return Value.ToUpper();
}
public string ToUpperInvariant()
{
Value = Value ?? "";
return Value.ToUpperInvariant();
}
public string Trim()
{
Value = Value ?? "";
return Value.Trim();
}
public string Trim(params char[] trimChars)
{
Value = Value ?? "";
return Value.Trim(trimChars);
}
public string TrimEnd(params char[] trimChars)
{
Value = Value ?? "";
return Value.TrimEnd(trimChars);
}
public string TrimStart(params char[] trimChars)
{
Value = Value ?? "";
return Value.TrimStart(trimChars);
}
}
You are on the right track because you can create a value type (struct
) to wrap a .NET primitive type and add some rules around the type without adding any real overhead.
The only problem is that value types can be default initialized exactly as a string can be default initialized. So you cannot avoid that there exists an "invalid" or "empty" or "null" value.
Here is a class that wraps a string with the added rule that the string cannot be null or empty. For lack of better name I decided to call it Text
:
struct Text : IEquatable<Text> {
readonly String value;
public Text(String value) {
if (!IsValid(value))
throw new ArgumentException("value");
this.value = value;
}
public static implicit operator Text(String value) {
return new Text(value);
}
public static implicit operator String(Text text) {
return text.value;
}
public static Boolean operator ==(Text a, Text b) {
return a.Equals(b);
}
public static Boolean operator !=(Text a, Text b) {
return !(a == b);
}
public Boolean Equals(Text other) {
return Equals(this.value, other.value);
}
public override Boolean Equals(Object obj) {
if (obj == null || obj.GetType() != typeof(Text))
return false;
return Equals((Text) obj);
}
public override Int32 GetHashCode() {
return this.value != null ? this.value.GetHashCode() : String.Empty.GetHashCode();
}
public override String ToString() {
return this.value != null ? this.value : "N/A";
}
public static Boolean IsValid(String value) {
return !String.IsNullOrEmpty(value);
}
public static readonly Text Empty = new Text();
}
You do not have to implement the IEquatable<T>
interface but it is a nice addition because you have to override Equals
anyway.
I decided to create two implicit cast operators so this type can be used interchangeably with normal strings. However, implicit cast can be a bit subtle so you might decide to change one or both into explicit cast operators. If you decide to use implicit casts you should probably also override the ==
and !=
operator to avoid using the ==
operator for strings when you really want to use Equals
for this type.
You can use the class like this:
var text1 = new Text("Alpha");
Text text2 = "Beta"; // Implicit cast.
var text3 = (Text) "Gamma"; // Explicit cast.
var text4 = new Text(""); // Throws exception.
var s1 = (String) text1; // Explicit cast.
String s2 = text2; // Implicit cast.
However, you still have a "null" or "empty" value:
var empty = new Text();
Console.WriteLine(Equals(text, Text.Empty)); // Prints "True".
Console.WriteLine(Text.Empty); // Prints "N/A".
This concept can easily be extended to more complex "strings", e.g. phone numbers or other strings with a structure. This will allow you to write code that is easier to understand. E.g., instead of
public void AddCustomer(String name, String phone) { ... }
you can change it to
public void AddCustomer(String name, PhoneNumber phone) { ... }
The second function does not need to validate the phone number because it already is a PhoneNumber
that has to be valid. Compare that to a string that can have any content and in each call you have to validate it. Even though that most seasoned developers probably will agree that it is a bad practice to use strings for string like values like social security numbers, phone numbers, country codes, currencies etc. it seems to be a very common approach.
Note that this approach does not have any overhead in terms of heap allocations. This is simply a string with some extra validation code.