3
\$\begingroup\$

In some solutions I use case-insensitive strings a lot and so far I've been always writing custom collections or using custom comparers to support them. This is usually a lot of work and a lot of testing so I thought what if the string was case-insensitive instead?

The implementation is really simple because the CaseInsensitiveString is just a wrapper for a string that is using the StringComparer.OrdinalIgnoreCase comparer internally. Can you think of any improvements? I use it for hash-sets or dictionaries or anything else that might require case insensitive strings.

public class CaseInsensitiveString : IEquatable<CaseInsensitiveString>, IEquatable<string>
{
    private readonly string value;

    public CaseInsensitiveString() { }

    public CaseInsensitiveString(string value)
    {
        this.value = value;
    }

    private CaseInsensitiveString(CaseInsensitiveString other)
    {
        value = other?.value;
    }

    public static CaseInsensitiveString Empty => new CaseInsensitiveString(string.Empty);

    public override int GetHashCode()
    {
        return StringComparer.OrdinalIgnoreCase.GetHashCode(value);
    }

    public override bool Equals(object obj)
    {
        return
            (obj is CaseInsensitiveString cis && Equals(cis)) ||
            (obj is string s && Equals(s));
    }

    public bool Equals(CaseInsensitiveString other)
    {
        if (ReferenceEquals(other, null)) return false;
        return ReferenceEquals(this, other) || Equals(other.value);
    }

    public bool Equals(string other)
    {
        return StringComparer.OrdinalIgnoreCase.Equals(value, other);
    }

    public static implicit operator string(CaseInsensitiveString obj) => obj.value;

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

    public static bool operator ==(CaseInsensitiveString left, CaseInsensitiveString right) => new CaseInsensitiveString(left).Equals(right);

    public static bool operator !=(CaseInsensitiveString left, CaseInsensitiveString right) => !(left == right);
}

Example usage:

var set = new HashSet<CaseInsensitiveString>();
set.Add("foo").Dump(); // true
set.Add("fOo").Dump(); // false

var s = (CaseInsensitiveString)"foo";
(s == "foo").Dump(); // true
(s == "fOo").Dump(); // true
\$\endgroup\$
8
  • 1
    \$\begingroup\$ Is there a particular reason you went with this instead of forcing everything into lowercase instead? That's how I usually check case-independent and I thought this was a common approach. \$\endgroup\$
    – Mast
    Commented Jul 9, 2017 at 13:43
  • \$\begingroup\$ @Mast you've just named the reason ;-) I don't want to think about it anymore and it's very easy to forget or to use a proper comaparison. Another reason is that you have to test all over again the same things to make sure they are really case-insensitive. \$\endgroup\$
    – t3chb0t
    Commented Jul 9, 2017 at 13:53
  • \$\begingroup\$ What is wrong with just initializing the collection with a case insensitive comparer? new HashSet<string>(StringComparer.OrdinalIgnoreCase); \$\endgroup\$
    – paparazzo
    Commented Jul 9, 2017 at 14:56
  • \$\begingroup\$ @Paparazzi in certain situations you cannot force case-insensivity for example for aa property like public ISet<string> Something { get; set; } \$\endgroup\$
    – t3chb0t
    Commented Jul 9, 2017 at 15:31
  • 1
    \$\begingroup\$ @Denis almost, I've just realized that I forgot to override ToString but yes, it's my full class but this one method. \$\endgroup\$
    – t3chb0t
    Commented Jul 9, 2017 at 18:03

2 Answers 2

1
\$\begingroup\$

I feel like this class should be more of a wrapper class than a standalone object, something like the Nullable<T>.


Janos pointed that there are some errors with the equality of 2 null objects, but there is even more edge cases!

Take this for example:

CaseInsensitiveString test1 = null;
CaseInsensitiveString test2 = new CaseInsensitiveString();
Console.WriteLine(test1 == test2);

This will print True, but reversing the order of the items:

Console.WriteLine(test1 == test2);

Would print False.

This is also known as Symmetric Equality. How does string deal with that?

Well first off string doesn't have an empty constructor. I don't know all the reasons why the designers didn't include one but this could very easily be one of them. It's unclear whether the value is null or empty. You already provide solutions for those options which are much more understandable, you can set the value to CaseInsensitiveString.Empty or you can just give it a value null.

But let's at least try to simulate this and see the output:

string test1 = null;
string test2 = new string(null as char[]);

This will print False in both cases, because the value of test2 would be string.Empty. Because your class is supposed to give just some extra functionality you should pretty much copy the behavior of the class you're 'extending', not modify it.


I'm not sure if the casting should be implicit:

string test1 = string.Empty;
CaseInsensitiveString test2 = string.Empty;
Console.WriteLine(test1 == test2);

Which comparison would be used here? Can you tell me right away if you didn't know what's written in your class? If you guess correctly that the CaseInsensitiveString equality methods will be used, what happens with the string? Would it be treated as a normal string or as a CaseInsensitiveString?

Tho this could be just me, I haven't looked much into how some predefined .net types do the same job.


Besides all that, your class is extremely specific, it doesn't have any string instance methods or any extension methods, it doesn't inherit IEnumerable<char> and many characteristics that string has, all you can do with it is to add items to a HashMap without worrying about the letter case. The usage shrinks even more when you think about it because most collections such as Dictionary<TKey, TValue> and HashSet<T> already have a constructor which can allow you to ignore case sensitivity.

You should at the very least expose a get only property with the current string value, so that the user can access it from there.

An alternative would be to have all the methods implemented in your class as instance methods, but that's not a good idea, as you would still need to write 2 extension methods if you want something to be able to operate on both the string and your type.

\$\endgroup\$
4
  • \$\begingroup\$ The idea with the decorator is very interesting and it reminds me of the _monads series on FAIC. I actually indend to use this type exclusively for keys/names so the other string methods are not a concern... yet. But I need to improve it. \$\endgroup\$
    – t3chb0t
    Commented Jul 9, 2017 at 19:20
  • \$\begingroup\$ I think I've fixed most issues already however I wonder which two extensions you mean? \$\endgroup\$
    – t3chb0t
    Commented Jul 9, 2017 at 20:32
  • \$\begingroup\$ @t3chb0t If there's no way to access the string value and you want to have some extension method for the string class you will need to write one for CaseInsensitiveString too, which would most likely modify the string before passing it to the constructor of the CaseInsensitiveString class, which isn't a really nice thing to have. It seems reasonable to have a Value property just like the Nullable<T> would have. \$\endgroup\$
    – Denis
    Commented Jul 9, 2017 at 20:37
  • 1
    \$\begingroup\$ The Nullable<T> is cheating because it has a compiler support ;-) I can't have this advantage. Making the value private was acutally intentional because I don't want to loose case insensivity by accident (which is why I also made one of the operators explicit based on your feedback). This makes writing extensions pretty impossible without calling ToString first or casting but this shouldn't be necessary as I use it almost exclusively for keys, names, or ids. I've added a few more methods for strings but that should be all I ever need. \$\endgroup\$
    – t3chb0t
    Commented Jul 9, 2017 at 20:44
1
\$\begingroup\$

Creating a throw-away object just to perform == comparison is ugly, and likely inefficient:

public static bool operator ==(CaseInsensitiveString left, CaseInsensitiveString right) => new CaseInsensitiveString(left).Equals(right);

I suspect you wanted to avoid a tedious null-check, but I don't think you have a choice:

public static bool operator ==(CaseInsensitiveString left, CaseInsensitiveString right) => !ReferenceEquals(left, null) && left.Equals(right);

But there's a more serious problem. As it stands, two null instances of CaseInsensitiveString are not equal, and it should be.

\$\endgroup\$
1
  • \$\begingroup\$ You're right, I wanted to reuse the already implemented comparer. \$\endgroup\$
    – t3chb0t
    Commented Jul 9, 2017 at 15:32

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.