【问题标题】:How can System.String be properly wrapped for case-insensitivy?System.String 如何正确包装以不区分大小写?
【发布时间】:2016-01-07 11:09:20
【问题描述】:

这个问题不是关于管理 Windows 路径名的;我仅将其用作不区分大小写字符串的特定示例。 (而且我现在换个例子,一大堆cmets就没有意义了。)


这可能类似于Possible to create case insensitive string class?,但那里没有太多讨论。另外,我并不关心string 所享受的紧密语言集成或System.String 的性能优化。

假设我使用了很多(通常)不区分大小写的 Windows 路径名(我实际上并不关心实际路径的许多细节,例如 \/\\\\ 相同如\file:// URL、.. 等)。一个简单的包装器可能是:

sealed class WindowsPathname : IEquatable<WindowsPathname> /* TODO: more interfaces from System.String */
{
    public WindowsPathname(string path)
    {
        if (path == null) throw new ArgumentNullException(nameof(path));
        Value = path;
    }

    public string Value { get; }

    public override int GetHashCode()
    {
        return Value.ToUpperInvariant().GetHashCode();
    }

    public override string ToString()
    {
        return Value.ToString();
    }

    public override bool Equals(object obj)
    {
        var strObj = obj as string;
        if (strObj != null)
            return Equals(new WindowsPathname(strObj));

        var other = obj as WindowsPathname;
        if (other != null)
            return Equals(other);

        return false;
    }
    public bool Equals(WindowsPathname other)
    {
        // A LOT more needs to be done to make Windows pathanames equal.
        // This is just a specific example of the need for a case-insensitive string
        return Value.Equals(other.Value, StringComparison.OrdinalIgnoreCase);
    }
}

是的,System.String 上的所有/大部分接口可能都应该实现;但以上内容似乎足以用于讨论目的。

我现在可以写了:

var p1 = new WindowsPathname(@"c:\foo.txt");
var p2 = new WindowsPathname(@"C:\FOO.TXT");
bool areEqual = p1.Equals(p2); // true  

这让我可以在我的代码中“谈论”WindowsPathnames,而不是像StringComparison.OrdinalIgnoreCase 这样的实现细节。 (是的,这个特定的类也可以扩展为处理\/,这样c:/foo.txt就等于C: \FOO.TXT; 但这不是这个问题的重点。)此外,当将实例添加到集合时,此类(带有附加接口)将不区分大小写;没有必要指定IEqualityComparer。最后,像这样的特定类还可以更轻松地防止“无意义”操作,例如将文件系统路径与注册表项进行比较。

问题是:这种方法会成功吗?是否有任何严重和/或微妙的缺陷或其他“陷阱”?(再次,与尝试设置不区分大小写的字符串类有关,而不是管理 Windows 路径名。)

【问题讨论】:

  • 请注意,更多的 '/' 或 '\' 可以使您的窗口路径不同的字符串但等于窗口路径。检查此路径规范化以了解更多信息stackoverflow.com/questions/2281531/…
  • 我不确定在必要时使用字符串不敏感比较有什么问题,因为您唯一关心它的时间是在比较期间。好像用大锤敲钉子,使用现有框架代码很容易解决问题的开销很大。
  • 这是一个大错误,从不区分大小写的比较开始严重依赖于文化,一个可以更改的属性,并以确切的路径名比较和解析规则作为文件系统驱动程序实现细节结束。 .NET 没有这个的基本原因。正确执行此操作的唯一方法是同时访问磁盘并检查您是否获得相同的文件系统路径。
  • 为字符串添加一个扩展方法也很简单,比如public static bool InsensitveCompare(this string a, string b) { return string.Compare(a, b, true) == 0; } 但是你的观点只是一个例子,你在哪里停下来? InsensitiveFirstName InsenstiveLastName, InsensitiveDog, InsensitiveCat 令人作呕。对于一个不断增长的问题,它只是膨胀,只需一行代码即可解决。
  • 如果你尝试过,你不可能选择一个更糟糕的例子:)

标签: c# .net string


【解决方案1】:

嗯...我不认为字符串大小写是您面临的唯一挑战。让我问你几个问题:

c:\myPathc:/myPath 一样吗? file:////c:/myPath 怎么样?或者\\myMachine\c$\myPath怎么样?

我有点理解你的目标和你想要完成的事情,但你似乎对一个简单的问题有远见 - 为什么要构建一个框架来做简单的 .ToLower()ToLower() 比较有吗?

话虽如此,如果您的问题范围除了字符串大小写,还涉及尝试评估两个给定路径的绝对相等性,那么编写一个类是有意义的。但这需要比您提出的解决方案更多的解决方案......

【讨论】:

    【解决方案2】:

    一种更短更轻量级的方法可能是创建一个扩展方法:

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

    与创建一个完整的单独类相比,这需要的编码要少得多,没有性能开销(甚至可能被内联),没有额外的分配,并且还非常清楚地表达了 IMO 的意图:

    var arePathsEqual = @"c:\test.txt".IsSamePathAs(@"C:\TEST.txt");
    

    【讨论】:

    • @Dan:你能解释一下吗?不确定你的意思。你是说你有一个反对为字符串编写扩展方法的个人“规则”吗? 1)你为什么不在收藏品上使用它们?您可以毫无问题地在IEnumerable&lt;T&gt; 上使用它们。那么困扰你的是索引器吗?你是想说他们不应该mutate 对象吗?你认为你可以改变字符串吗? 2)String 甚至不是一个集合。它有一个(只读)索引器,但它只实现IEnumerable&lt;char&gt;,没有其他集合接口。所以,当你写 "stuff".Skip(1) 时,是的,你正在使用扩展。
    【解决方案3】:

    我将创建一个包含字符串的不可变结构,将构造函数中的字符串转换为标准大小写(例如小写)。 然后您还可以添加隐式运算符以简化创建并覆盖比较运算符。 我认为这是实现该行为的最简单方法,而且您只会获得很小的开销(转换仅在构造函数中)。

    代码如下:

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

    用法如下:

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

    这也适用于集合。

    【讨论】:

    • 这是个好问题。在这种特殊情况下,这只是个人喜好问题,因为它是不可变的,并且封装了一个字段。重要的是不仅 System.String 是一个引用类型,而且它也是不可变的。因此,即使该字段是公开的,它也是不可变的。所以使用一个类是完全没问题的,而且行为也是一样的。
    • 除了不变性之外,还有性能需要考虑,struct 也在这里获胜;它是一个围绕单个引用/指针的轻量级包装器,复制起来很便宜(可能与引用本身一样便宜),但不会受到class 所需的额外间接级别的影响。
    【解决方案4】:

    所以你想要一个将字符串转换为对象的东西,如果你将两个字符串转换为其中两个对象,你希望能够将这些对象的相等性与你自己的一组关于相等的规则进行比较两个对象。

    在您的示例中,它是关于大写和小写的,但也可能是关于正斜杠和反斜杠的,也许您甚至想定义“单词” USD 等于 $。

    假设您将所有可能的字符串集合划分为您定义为相等的字符串子集合。在这种情况下,“Hello”将与“HELLO”和“hElLO”在同一个子集合中。也许“c:\temp”会和“c:/TEMP”在同一个集合中。

    如果您能找到一些东西来标识您的子集合,那么您可以说属于同一子集合的所有字符串都具有相同的标识符。或者换句话说:您定义为相等的所有字符串都将具有相同的标识符。

    如果有可能,那么比较子集合标识符就足够了。如果两个字符串具有相同的子集合标识符,那么它们属于同一个子集合,因此根据我们的相等定义被认为相等。

    我们称这个标识符为字符串的标准化值。 CaseInsensitiveString 的构造函数可以将输入字符串转换为字符串的规范化值。要检查两个对象是否相等,我们要做的就是检查它们是否具有相同的标准化值。

    字符串规范化的一个例子是:

    • 使字符串小写
    • 使所有斜线都变成反斜线
    • 将所有单词 USD 转换为 $
    • 在没有千位分隔符的数字中删除所有千位分隔符
    • 等,具体取决于您希望字符串何时相等。

    根据上述情况,以下字符串都将导致相同的规范化字符串:

    • 白宫 $1,000,000
    • 白宫100万美元
    • 白宫100万美元

    我们可以将任何东西定义为规范化字符串,只要我们定义为 equal 的所有字符串都具有相同的规范化字符串。一个很好的例子是

    • 白宫100万美元

    注意:我不会详细介绍如何查找 USD 和千位分隔符等单词。重要的是你理解规范化字符串的含义。

    说了这么多,唯一困难的部分就是找到stringIdentifier。课程的其余部分相当简单:

    构造代码。构造函数接受一个字符串并确定它所属的子集合。我还添加了一个默认构造函数。

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

    相等:根据定义,如果两个对象具有相同的标准化值,则它们是相同的

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

    请注意,最后一行是我们进行实际相等性检查的唯一代码!

    所有其他相等函数只使用上面定义的 Equals 函数:

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

    所以现在您可以执行以下操作:

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

    现在我们唯一需要实现的是 Normalize 函数。一旦你知道两个字符串何时被认为相等,你就知道如何规范化。

    假设两个字符串相等,如果它们不区分大小写并且正斜杠与反斜杠相同。 (英语不好)

    如果 normalize 函数返回相同的带有反斜杠的小写字符串,那么我们认为相等的两个字符串将具有相同的标准化值

    private string Normalize(string str)
    {
        return str.ToLower().Replace('/', '\');
    }
    

    【讨论】:

      【解决方案5】:

      首先,直言不讳
      您必须定义该类的明确职责。

      您希望该类管理 Windows 路径名称,然后您不能放弃所有关于此的注释,因为“代码管理案例”将与“代码管理路径”合并。耦合将使您无法在不考虑路径的情况下测试(并确保正确的行为)套管。

      或者你想实现一个CaseInvariantString,然后你适当地命名它(也许在另一个名为WindowsPathname的类中使用它)。

      关于Class Responbility、Cohesion、Coupling和其他伟大概念的参考,我推荐以下书籍:

      • 清洁代码来自 Robert C. Martin(鲍勃叔叔)
      • 代码完成来自 Steeve McConnell

      其次,在类中包装字符串以检查大小写不变性可以看作是在 PositiveInteger 类中包装整数。 有些人可能(并且将会)认为这是过度设计。尝试达到面向对象教条的顶峰是所有开发人员的共同趋势。这里看起来像是将所有值类型包装在类中的做法(如将 int 包装到 ID 类中)。但是,不要忘记向您提问。

      • 采用这种做法的成本是多少?
      • 有什么好处?
      • 它可能会导致什么困难?
      • 我可以对我的所有项目采用通用方法吗?
      • 我的技术主管/架构师(或类似权威机构)是否制定了一项良好做法?

      最后,作为一个简单的技术点。 你不应该在你的类中创建一个字符串。这对性能有害。事实上,因为字符串是不变的,所以当你在GetHashCode() 中执行ToUpperInvariant() 时,它会创建一个新的String

      而且为了路径不变性...它在 Windows 之外不起作用
      对于 Mono,显然是 "/foo" != "/Foo")。

      【讨论】:

        猜你喜欢
        • 2017-02-21
        • 2013-08-05
        • 2012-02-18
        • 1970-01-01
        • 2013-03-06
        • 2011-01-28
        • 2023-04-02
        • 2011-10-14
        相关资源
        最近更新 更多