简体   繁体   中英

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

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?

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", eg phone numbers or other strings with a structure. This will allow you to write code that is easier to understand. Eg, 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.

随着 2019 年 4 月 C# 8 和可为空引用类型的发布,这现在是一种语言功能。

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.

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.

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.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM