How can System.String be properly wrapped for case-insensitivy?

前端 未结 5 1696
谎友^
谎友^ 2021-01-12 07:05

This question is not about managing Windows pathnames; I used that only as a specific example of a case-insensitive string. (And I if I change the example now, a w

相关标签:
5条回答
  • 2021-01-12 07:08

    First, Call a Spade a Spade
    You must define what is the clear responsibility of the class.

    Either you want the class to manage Windows Path names and then you can't discard all remarks regarding that because the "code managing case" will be merged with the "code managing paths". Coupling will make you then unable to test (and ensure proper behaviour) casing without path consideration.

    Or you want to implement a CaseInvariantString and then you name it adequately (and maybe use it in another class named WindowsPathname).

    For references about Class Responbility, Cohesion, Coupling and other great concepts, I would recommend the following books:

    • Clean Code from Robert C. Martin (Uncle Bob)
    • Code Complete from Steeve McConnell

    Second, wrapping strings inside a class to check for case invariance can be viewed as wrapping integer in a PositiveInteger class. It can (and will) be considered by some as over-engineering. It is a common tendency from all developers to try to reach the pinnacle of object oriented dogma. Here it seems like the practice of wrapping all value types in class (like int into an ID class). However, don't forget to ask you questions.

    • What is the cost of adopting such practice?
    • What are the benefits?
    • What are the difficulties it may lead to?
    • Could I adopt the general approach for all my projects?
    • Do I have an enactment of my tech lead/architect (or similar authority) that it is a good practice?

    Finally, as a simple technical point. You shouldn't create a string inside your class. It's deleterious for performance. In fact, because strings are invariant, when you do a ToUpperInvariant() in GetHashCode() it creates a new String.

    And for the sake of path invariance... It doesn't work outside Windows
    (For Mono, obviously "/foo" != "/Foo").

    0 讨论(0)
  • 2021-01-12 07:11

    So you want a something that converts a string to an object, and if you convert two strings to two of those objects, you want to be able to compare these objects for equality with your own set of rules about the equality of the two objects.

    In your example it is about upper and lower case, but it could also be about forward slashes and backward slashes, maybe you even want to define that the "word" USD equals to $.

    Suppose you divide the collection of all possible strings in subcollections of strings that you'd define to be equal. In that case "Hello" would be in the same subcollection as "HELLO" and "hElLO". Maybe "c:\temp" would be in the same collection as "c:/TEMP".

    If you could find something to identify your subcollection, then you could say that all strings that belong to the same subcollection would have the same identifier. Or in other words: all strings that you defined equal would have the same identifier.

    If that would be possible, then it would be enough to compare the subcollection identifier. If two strings have the same subcollection identifier, then they belong to the same subcollection and thus are considered equal according to our equality definition.

    Let's call this identifier the normalized value of the string. The constructor of your CaseInsensitiveString could convert the input string into the normalized value of the string. To check two objects for equality all we have to do is check if they have the same normalized value.

    An example of the normalization of a string would be:

    • Make the string lower case
    • make all slashes backward slashes
    • convert all words USD to $
    • remove all thousand separators in numbers without thousand seperator
    • etc, depending on when you want the strings to be equal.

    According to the above the following Strings would all lead to the same normalized string:

    • White House $ 1,000,000
    • White House $ 1000000
    • white house USD 1000000

    We can define anything as a normalized string, as long as all strings that we define equal have the same normalized string. A good example would be

    • white house $ 1000000

    Note: I'm not going into detail about how to find words like USD and thousand separator. The importance is that you understand the meaning of normalized string.

    Having said this, the only difficult part is to find the stringIdentifier. The rest of the class is fairly straightforward:

    Code for the construction. The constructor takes a string and determines the subcollection it belongs to. I also added a default constructor.

    public class CaseInsensitiveString : IEquatable<CaseInsensitiveString>
    {
        private string normalized = "";
    
        public CaseInsensitiveString(string str)
        {
            this.normalized = Normalize(str);
        }
    
        public CaseInsensitiveString()
        {
             this.Normalize = Normalize(null);
        }
    }
    

    Equality: by definition, two objects are the same if they have the same normalized value

    See MSDN How to Define Value Equality for a Type

    public bool Equals (CaseInsensitiveString other)
    {
        // false if other null
        if (other != null) return false;
    
        // optimization for same object
        if (object.ReferenceEquals(this, other) return true;
    
        // false if other a different type, for instance subclass
        if (this.Gettype() != other.Gettype()) return false;
    
        // if here: everything the same, compare the stringIdentifier
        return this.normalized==other.normalized;
    }
    

    Note that this last line is the only code where we do actual equality checking!

    All other equality functions only use the Equals function defined above:

    public override bool Equals(object other)
    {
        return this.Equals(other as CaseInsensitiveString);
    }
    
    public override int GetHashCode()
    {
        return this.Normalized.GetHashCode();
    }
    
    public static bool operator ==(CaseInsensitiveString x, CaseInsensitiveString y)
    {
        if (object.ReferenceEquals(x, null)
        {   // x is null, true if y also null
            return y==null;
        }
        else
        {   // x is not null
            return x.Equals(y);
        }
    }
    
    public static bool operator !=(CaseInsensitiveString x, CaseInsensitiveString y)
    {
        return !operator==(x, y);
    }
    

    So now you can do the following:

    var x = new CaseInsensitiveString("White House $1,000,000");
    var y = new CaseInsensitiveString("white house $1000000");
    if (x == y)
        ...
    

    Now the only thing we have to implement is the Normalize function. Once you know when two strings are considered equal you know how to normalize.

    Suppose consider two strings equal if they are equal case insensitive and forward slashes are the same as backward slashes. (bad English)

    If the normalize function returns the same string in lower case with all backward slashes, then two strings that we consider equal will have the same normalized value

    private string Normalize(string str)
    {
        return str.ToLower().Replace('/', '\');
    }
    
    0 讨论(0)
  • 2021-01-12 07:22

    I would create an immutable struct that hold a string, converting the string in the constructor to a standard case (e.g. lowercase). Then you could also add the implicit operator to simplify the creation and override the compare operators. I think this is the simplest way to achieve the behaviour, plus you get only a small overhead (the conversion is only in the constructor).

    Here's the code:

    public struct CaseInsensitiveString
    {
        private readonly string _s;
    
        public CaseInsensitiveString(string s)
        {
            _s = s.ToLowerInvariant();
        }
    
        public static implicit operator CaseInsensitiveString(string d)
        {
            return new CaseInsensitiveString(d);
        }
    
        public override bool Equals(object obj)
        {
            return obj is CaseInsensitiveString && this == (CaseInsensitiveString)obj;
        }
    
        public override int GetHashCode()
        {
            return _s.GetHashCode();
        }
    
        public static bool operator ==(CaseInsensitiveString x, CaseInsensitiveString y)
        {
            return x._s == y._s;
        }
    
        public static bool operator !=(CaseInsensitiveString x, CaseInsensitiveString y)
        {
            return !(x == y);
        }
    }
    

    Here is the usage:

    CaseInsensitiveString a = "STRING";
    CaseInsensitiveString b = "string";
    
    // a == b --> true
    

    This works for collections as well.

    0 讨论(0)
  • 2021-01-12 07:30

    A shorter and more lightweight approach might be to create an extension method:

    public static class StringExt
    {
        public static bool IsSamePathAs(this string @this, string other)
        {
            if (@this == null)
                return other == null;
    
            if (object.ReferenceEquals(@this, other))
                return true;
    
            // add other checks
            return @this.Equals(other, StringComparison.OrdinalIgnoreCase);
        }
    }
    

    This requires far less coding than creating a whole separate class, has no performance overhead (might even get inlined), no additional allocations, and also expresses the intent pretty clearly IMO:

    var arePathsEqual = @"c:\test.txt".IsSamePathAs(@"C:\TEST.txt");
    
    0 讨论(0)
  • 2021-01-12 07:31

    Hmm... I don't think string case is the only challenge you have. Let me ask you a couple of questions:

    Is c:\myPath the same as c:/myPath? How about file:////c:/myPath? Or how about \\myMachine\c$\myPath?

    I kind of understand where you are headed and what you want accomplished, but it just seems like you're tunnel-visioned on a simple problem - why build a framework that does what a simple .ToLower() vs ToLower() comparison does?

    That being said, if your problem scope, in addition to the string casing, involves trying to assess absolute equality of two given paths, it makes sense to write up a class. But that would require a lot more involved solution than what you are proposing...

    0 讨论(0)
提交回复
热议问题