C# creating a non-nullable string. Is it possible? Somehow?
Asked Answered
H

8

31

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?

Hansom answered 24/10, 2014 at 18:25 Comment(11)
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.Zrike
@Zrike 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.Hallo
@Zrike 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)?Hansom
Your example of the struct is how ASP.NET Web Forms controls do it.Kickback
so...you're trying to create a mutable struct? You, my good sir, are the devil. ;)Misguided
@K.AlanBates: Actually, a struct which encapsulates a single immutable reference of type String, implements widening conversion operators to and from String, and chains all the methods of String to the encapsulate instance when non-null, or behaves like an empty string when null, would behave almost exactly like a String whose default value was an empty string rather than null; the only leak in the abstraction would be that such a structure would box as its own type rather than String.Anselmi
@Anselmi That's not the struct being referred to though. Look at the OP's struct Struct. He wants to create a struct with two mutable non-nullable string references. Creating a struct that represents a non-nullable string reference wouldn't even need to be mutable in the first place.Repudiation
My wish has been for .NET to make strings behave as value type so that there can be string and string?. This would make it ultra clear whether a string is ALLOWED to be null or not. Unfortunately we're left with the broken faux-immutable implementation of string as a reference type that masquerades as a value type. The fact that under the hood it's implemented as a reference type, with the interning stuff, should just be abstracted from me.Lohr
@ChrisMarisic: I would have liked to have seen a type StringObject which behaves as String does not, and StringValue which would behave like a value type that encapsulated a sequence of characters (implemented most likely as a struct holding a single private reference of type StringObject). For efficiency, boxing a StringValue should yield either the encapsulated StringObject or a reference to String.Empty, and it should be possible to unbox a StringObject into a StringValue.Anselmi
@ChrisMarisic as implemented in the CLR, strings (and -by extension- BSTRs from ActiveX Automation) have always felt to me like a crippled hack rather than a language feature....stupid Visual Basic.Misguided
@K.AlanBates: Just as there are times when it's useful to rely upon the fact that arrays of numbers default to zero, so too in some other languages there were times when it is useful to rely upon arrays of strings defaulting to empty strings. The fact that .NET can't provide such behavior doesn't mean there's anything wrong with platforms that can (such as VB6). There are a lot of other things wrong with VB6, but I'm sure what you're blaming it for here.Anselmi
H
27

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.

Hafiz answered 24/10, 2014 at 18:38 Comment(8)
Beat me to it. I was typing up almost this exact answer.Enure
@Enure Finally the typing course I followed pays off!Hafiz
@Hafiz 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?Tymon
@Tymon I don't know the statistics, but at least the conversion is simple and clear to anyone who knows what nString stands for.Hafiz
I'd upvote twice or more, seriously. Copied as it is, the only difference for me is an exception in the ctor if someone try to assign it a null or empty value. Nice job!!!Oys
Instead of "N/A" you should throw an exception. This is only changing the null problem to the string "N/A". How do you differentiate between a valid value of "N/A" and the default value of "N/A"?Enrich
@Enrich 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.Hafiz
I think it is harder than this, and that your nString.Value is still null by default. dotnetfiddle.net/pjGe78Jurisdiction
H
4

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);
    }
}
Hansom answered 24/10, 2014 at 21:9 Comment(0)
R
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.

Roan answered 24/10, 2014 at 19:5 Comment(6)
I'd suggest having the comparison method compare the ToString() values of both instances and having GetHashCode return ToString().GetHashCode().Anselmi
@supercat: But then you cannot tell the difference between Text.Empty and new Text("N/A").Roan
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.Anselmi
@supercat: Thanks for the input about the bug. I should have fixed it now.Roan
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.Anselmi
Such a design would be a little icky in some ways, but it would have the advantage that a class could easily have multiple string-ish fields and separately control the defaults for them, without having to duplicate all the code for the Text class.Anselmi
G
2

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

Goring answered 5/7, 2019 at 8:3 Comment(3)
This is good info, but I don't think it meets the quality of a good answer. I would expand with an example.Babbage
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.Jurisdiction
See sharplab.io/…Jurisdiction
T
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.

Tymon answered 24/10, 2014 at 18:35 Comment(3)
calling it GetValueOrDefault is a bad name, as null is the default for string.Kickback
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.Tymon
Yea, I was just being pedantic.Kickback
A
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.

Anselmi answered 24/10, 2014 at 19:2 Comment(0)
V
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.

Vizzone answered 24/10, 2014 at 18:39 Comment(0)
O
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;
}
Occipital answered 6/1, 2021 at 8:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.