字符串比较:当为空时,null和空字符串相等。

55
使用C#和.NET 3.5,如何处理这种情况最好的方式。我有数百个字段需要从各种来源进行比较(大多为字符串)。有时源将字符串字段返回为空,有时返回为空字符。当然,有时候字段中有文本。我的当前比较strA!= strB并不起作用,因为例如strA为null而strB为""。我知道我可以使用string.IsNullOrEmpty来进行双重比较,但这样会产生一些不美观的问题。是否有更好的方法来处理这个问题?我想到扩展方法,但您无法扩展运算符。
我想找一个简洁明了的方式来解决这个问题。

2
你能否让“SOURCE”返回空字符串,或者希望有一个空字符串可用的null字符串? - Adriaan Stander
8个回答

92

虽然不能消除额外的底层比较,但为了增加其吸引力,您可以使用类似以下代码:

(strA ?? "") == (strB ?? "")

或者稍微没有那么吸引人,但更可取的形式:

(strA ?? string.Empty) == (strB ?? string.Empty)

嗯,我曾尝试使用扩展方法来避免重新执行成百上千次比较,但我想我必须得硬着头皮做了。我可能最终需要编写一个在后台执行此操作的方法。谢谢。 - billb
string.Empty并不是很紧凑(不够简洁),这就是为什么我两个都包含了,但是,使用string.Empty是首选。 - iammichael
"string.Empty 是首选" - Kevin Whitefoot
5
天啊!我在错误的时刻按下了回车键。本意是说: "建议使用 string.Empty"。为什么?虽然它更长,更丑,并且对运行时行为没有任何影响。请参阅其他关于 String.Empty 的 SO 条目和链接 (http://blog.codinghorror.com/micro-optimization-and-meatballs/)。 就我而言, "" 可以完全阅读,并且比 String.Empty 更可取。 - Kevin Whitefoot
可读性高?也许。可取性强?并非总是如此。string.Empty不会分配新的不可变字符串实例,用于池化(可能相当于每个程序集或每个AppDomain一个额外的对象引用,不确定这两者是否都正确)。就原始性能而言(在那篇文章和测试的机器/运行时上),有一些替代方案可以产生稍微更好的结果。我更喜欢string.Empty > String.Empty > "",仅仅因为我更喜欢"string"而不是"String",而且内联常量("")通常会被放置到const中,我的重构感官也会受到刺激!! - Graeme Wicksted

70

看起来你需要进行数百次比较,因此你想要一个单一的函数以便在代码中减少混乱和重复。我认为没有内置的函数可以将空值/空字符串/比较检查全部合并到一起,但你可以自己创建一个:

static class Comparison
{
    public static bool AreEqual(string a, string b)
    {
        if (string.IsNullOrEmpty(a))
        {
            return string.IsNullOrEmpty(b);
        }
        else
        {
            return string.Equals(a, b);
        }
    }
}

那么你可以为每个比较只使用一次你的函数:

        if(Comparison.AreEqual(strA[0], strB[0])) { // ... }
        if(Comparison.AreEqual(strA[1], strB[1])) { // ... }
        if(Comparison.AreEqual(strA[2], strB[2])) { // ... }
        if(Comparison.AreEqual(strA[3], strB[3])) { // ... }

如果您后来发现需要考虑其他情况(例如忽略字符串开头或结尾的空格),这种方法也更容易扩展;然后只需向函数添加更多逻辑以执行修剪等操作,而无需修改调用函数的数百行代码。


1
String.Equals(a,b) 也可以处理 null 比较,因此可以省略 string.IsNullOrEmpty() 的检查。 - abhaybhatia
4
@abhaybhatia的意思是,String.Equals(a,b)方法不会认为空字符串等于null,而它们实际上应该是相同的。因此,需要使用string.IsNullOrEmpty()方法进行检查。 - Dr. Wily's Apprentice

12

这并不像??那样引人注目,但是如果您使用短路运算符,可以避免时间的双重比较:

string.IsNullOrEmpty( strA ) ? string.IsNullOrEmpty( strB ) : (strA == strB )

6

关于什么?

strA ?? "" == strB ?? ""

3

这也可以工作,忽略大小写

(strA ?? "").Equals(strB ?? "", StringComparison.OrdinalIgnoreCase)

3

补充 几年后,写了几个相等比较器之后,我的观点已经改变,我认为最好的做法是让相等比较器拥有一个静态成员来保存创建的比较器,而不是每个用户都创建一个新实例。


(原始答案,按照上述调整)

其他人提供的解决方案,包括提出为字符串定义比较类的方案,忘记了为您的字符串编写新的GetHashCode

这意味着您的字符串无法在依赖于GetHashCode的类中使用,例如Dictionary<T>HashSet<T>

请参见为什么重写Equals方法时重写GetHashCode很重要?

每当您决定更改任何类的相等概念时,都应为该类编写一个EqualityComparer。这确保如果根据您更改的相等定义对象被视为相等,则它们的GetHashCode将返回相等的值。

public class NullStringComparer : EqualityComparer<string>
{
    public static IEqualityComparer<string> NullEqualsEmptyComparer {get} = new NullStringComparer();

    public override bool Equals(string x, string y)
    {
        // equal if string.Equals(x, y)
        // or both StringIsNullOrEmpty
        return String.Equals(x, y)
            || (String.IsNullOrEmpty(x) && String.IsNullOrEmpty(y));
    }

    public override int GetHashCode(string obj)
    {
        if (String.IsNullOrEmpty(obj))
           return 0;
        else
            return obj.GetHashCode();
    }
}

使用方法:

public static void Main()
{
    string x = null;
    string y = String.Empty;

    Console.WriteLine("Standard string comparison: {0}", 
        StringComparer.Ordinal.Equals(x, y));

    Console.WriteLine($"My string comparison {0}",
        NullStringComparer.NullEqualsEmpty.Equals(x, y));

    // because according to the NullStringComparer x equals y
    // GetHashCode should return the same value
    int hashX = NullStringComparer.NullEqualsEmpty.GetHashCode(x);
    int hashY = NullStringComparer.NullEqualsEmpty.GetHashCode(y);
    Console.WriteLine($"hash X = {hashX}, hash Y = {hashY}");
} 

2

string.IsNullOrEmpty()有什么问题吗?我确定它是.NET框架的一部分,它被优化过,可能比你我写的代码更高效。虽然它不太性感,但它很实用。编写易于阅读的代码,让编译器处理细节。


也许是这样,但是使用查找与替换功能会帮助您更新代码吧? - TLiebe
当然,但是你不打算替换数百个比较吗?你打算如何在没有手动干预的情况下替换!= - Chris Farmer
重载 != 以某种方式绕过代码更改。是的,搜索和替换也有帮助,但我说了“性感”,该死的。哈哈! - billb
1
@billb:快速性感还是漂亮性感?当if ((s1 ?? "") == (s2 ?? "")) 迭代10,000,000次时,大约需要155毫秒,而当if (string.IsNullOrEmpty(s1) && string.IsNullOrEmpty(s2))迭代10,000,000次时,只需要145毫秒(其中一个字符串为空,另一个为null)。我同意这种差异不值得担心,我只是指出“更好”总是在竞争需求的背景下。 - Binary Worrier
感谢Binary Worrier对这两种方法的比较。 - TLiebe
显示剩余2条评论

0

如果您的两组字段在某种集合中,您可能可以利用LINQ。如果它们在某种允许您通过键访问它们并且它们都具有相同键的集合中,则可以使用此代码(可直接粘贴到LINQPad):

Dictionary<string,string> fields1 = new Dictionary<string,string>();
Dictionary<string,string> fields2 = new Dictionary<string,string>();

fields1.Add("field1", "this");
fields2.Add("field1", "this");
fields1.Add("field2", "is");
fields2.Add("field2", "");
fields1.Add("field3", "a");
fields2.Add("field3", null);
fields1.Add("field4", "test");
fields2.Add("field4", "test");

var test = 
from f1 in fields1
    join f2 in fields2
    on f1.Key equals f2.Key
select (f1.Value ?? "") == (f2.Value ?? "");

test.Dump();

如果您有两个索引集合中的字段集以相同顺序排列,则可以使用以下代码:
string[] strings1 = { "this", "is", "a", "test" };
string[] strings2 = { "this", "", null, "test" };

var test = 
from s1 in strings1.Select((value,index) => new {value, index})
    join s2 in strings2.Select((value,index) => new {value, index})
    on s1.index equals s2.index
select (s1.value ?? "") == (s2.value ?? "");

test.Dump();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接