32

So you can't inherit string. You can't make a non-nullable string. But I want to do this. I want a class, let's call it nString that returns a default value when it would otherwise be null. I have JSON objects that might have who knows how many null strings, or even null objects. I want to create structs that have strings that will never return null.

public struct Struct
{
    public nString value;
    public nString value2;
}

I suppose I could do something like this:

public struct Struct
{
    public string val { get { return val ?? "N/A"; } set { val = value; } }
    public string val2 { get { return val2 ?? "N/A"; } set { val2 = value; } };
}

But that's so much more work. Is there any way to do this?

11
  • 4
    You should start to accept the null concept and not creating workarounds. A String with the null value has a different meaning than the empty string. So has a nulled Integer another meaning than 0. Commented Oct 24, 2014 at 18:30
  • 4
    @dognose although I don't disagree, it depends on his application. For his requirements null and the empty string might be interchangeable, and he wants to avoid NullReferenceExceptions. Commented Oct 24, 2014 at 18:32
  • 2
    @dognose Yes I realize that. But in this case I don't want null, ever. What good reason is there to not be able to make a non-nullable string? How much work could you save by not having to say if (strVal != null)? Commented Oct 24, 2014 at 18:33
  • 1
    Your example of the struct is how ASP.NET Web Forms controls do it. Commented Oct 24, 2014 at 18:34
  • 2
    so...you're trying to create a mutable struct? You, my good sir, are the devil. ;) Commented Oct 24, 2014 at 18:34

8 Answers 8

28

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.

Sign up to request clarification or add additional context in comments.

8 Comments

Beat me to it. I was typing up almost this exact answer.
@valverij Finally the typing course I followed pays off!
@C.Evenhuis I've seen this before, and fear that it's not as readable as an extension method. Do people tend to use implicit operators often?
@dbarnes I don't know the statistics, but at least the conversion is simple and clear to anyone who knows what nString stands for.
@t3dodson I just answered the question, where the OP specifically wants "a default value when it would otherwise be null". Perhaps you could comment to the question, but keep in mind it was asked in 2014.
|
6

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

Comments

3

With the release of C# 8 in April 2019 and nullable reference types this is now a language feature.

3 Comments

This is good info, but I don't think it meets the quality of a good answer. I would expand with an example.
nullable reference types do not help you enforce non-null strings as much as you'd hope. Especially, it does not help you with struct fields being initialised to null ; or with json deserialization resulting in nulls.
2

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.

6 Comments

I'd suggest having the comparison method compare the ToString() values of both instances and having GetHashCode return ToString().GetHashCode().
@supercat: But then you cannot tell the difference between Text.Empty and new Text("N/A").
I guess the question is whether one wants a type that behaves as a string with a default value, or one wants a type with a null default value but a ToString() method that returns something else. The former [with an empty default value] can be helpful when working with code that expects VB6-style semantics for strings, but I can see how the latter could be useful as well. On the other hand, calling GetHashCode on a structure isn't supposed to throw NullReferenceException so you might want to fix that.
@supercat: Thanks for the input about the bug. I should have fixed it now.
Incidentally, it would be possible to allow code to specify the default string value if your structure had a struct-constrained generic type parameter TDefault and, rather than returning a fixed string when value is null, it returned default(TDefault).ToString(). One could then easily define any number of structure types with any desired default ToString() values. One could then have variables of type Text<DefaultStringNA> return "N/A" as their default, variables of type Text<DefaultStringUnspecified> return "Unspecified", etc.
|
1

You could use something like an extension method

public static class StringExtensions
{
    public static string GetValueOrNotAvailable(this string value)
    {
        return value ?? "N/A";
    }
}

then you'd be able to call it like this

string s = (some variable that could be null)
Console.WriteLine(s.GetValueOrNotAvailable());

sadly you can't override string's get method, you could make a new type that keeps track of an internal string like you have above.

3 Comments

calling it GetValueOrDefault is a bad name, as null is the default for string.
True, The function name should be up to the developer, but this was just an example of how it could be used. I'll change it since I've seen enough people copy and paste off this site without giving any thought to it.
Yea, I was just being pedantic.
1

It's possible to define an "immutable"(*) struct which behaves almost exactly like a String, but has a default value that behaves like an empty string rather than null. Such a type should encapsulate a single field of type String or Object, define a narrowing conversion from String which ensures the supplied string is non-null and stores the it in its data field, and a widening conversion to String which returns an empty string if the field is null, or its ToString() value otherwise. For each public member of String, the type should define a member which invokes the corresponding member of (String)this. Such a type should also define overloads for string concatenation operators.

(*) All value types which can hold any value which is observably different from their default are mutable, since struct1 = struct2; will mutate the instance stored in struct1 by overwriting all its public and private fields with the contents of the corresponding fields in type2, and there's nothing the structure type can do to prevent that.

Although in most cases one would want to have such a type simply keep a reference to a String, there are some cases where it might be useful for it to do otherwise. For example, one could define one or more immutable "CompositeString" classes which would hold multiple strings, and have a ToString method which would concatenate them and cache the result. Using such types, it would be possible to make a loop like:

for (i=0; i<100000; i++)
  st = st + something;

yield performance that's almost within an order of magnitude of StringBuilder without having to make use of any observably-mutable class semantics (each iteration of the loop would generate a new CompositeString object, but a lot of information could be shared between objects).

Even if initially one never stores anything other than a String into the data field, using Object and calling ToString() on it would make it possible to other implementations should the need arise.

Comments

0

No, you cannot do this.

The only way to create a non-nullable type is to declare a struct - structs, however, cannot inherit or be inherited from.

Using properties as you are is most likely the best way, or null-coalescing during deserialization as previously suggested, but C# is simply designed to handle null values.

Comments

0

I was wondering about something like this for curiosity's sake and came across this question. The other answers don't seem to account for the cases of nString myString = new nString(); or nString myString = default.

In these cases, myString.Value will be equal to null as structs always have a parameterless constructor. This means that in the above cases, the code in the constructor that takes a string and replaces any null values is never called.

Here's a quick updated version that ensures the Value property will never return a null value. This uses C# 8's nullable reference types and C# 9's shorter 'new()' expression so you may have to change it slightly if you're not up-to-date.

public readonly struct nString
{
    private readonly string? _value;
    
    public string Value => _value ?? string.Empty; // Replace string.Empty with any other desired default value.

    public nString(string? value)
    {
        _value = value;
    }

    public static implicit operator nString(string? value) => new(value);

    public static implicit operator string(nString value) => value.Value;
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.