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
2 Answers 2
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.
-
\$\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– t3chb0t2017年07月09日 19:20:49 +00:00Commented 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– t3chb0t2017年07月09日 20:32:07 +00:00Commented 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 theCaseInsensitiveString
class, which isn't a really nice thing to have. It seems reasonable to have aValue
property just like theNullable<T>
would have. \$\endgroup\$Denis– Denis2017年07月09日 20:37:53 +00:00Commented 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 thevalue
private was acutally intentional because I don't want to loose case insensivity by accident (which is why I also made one of the operatorsexplicit
based on your feedback). This makes writing extensions pretty impossible without callingToString
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– t3chb0t2017年07月09日 20:44:30 +00:00Commented Jul 9, 2017 at 20:44
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.
-
\$\begingroup\$ You're right, I wanted to reuse the already implemented comparer. \$\endgroup\$t3chb0t– t3chb0t2017年07月09日 15:32:08 +00:00Commented Jul 9, 2017 at 15:32
public ISet<string> Something { get; set; }
\$\endgroup\$ToString
but yes, it's my full class but this one method. \$\endgroup\$