CodeGym /课程 /C# SELF /复杂比较场景和最佳实践

复杂比较场景和最佳实践

C# SELF
第 30 级, 课程 3
可用

1. 多级(层次)排序

为什么比较不总是那么简单?

C#面试经常喜欢问复杂对象的排序和比较问题。这不是随便问问,而是因为开发中真的会遇到。比如查数据库、界面上表格排序、集合里判断唯一性——这些都和对象比较的正确性直接相关。

比数字很简单。但如果我们想按姓排序用户,再按名排序,还要考虑有些字段可能是空的(null),或者字符串是不同语言写的……这时候就得系统性地处理了。

常见需求:先按一个条件排,如果相等再按另一个,如果还相等再按第三个。

例子:有名、姓和出生日期的用户


public class User
{
    public string FirstName { get; set; }
    public string LastName  { get; set; }
    public DateTime BirthDate { get; set; }

    // 为了美观——输出用户信息
    public override string ToString()
        => $"{LastName} {FirstName} ({BirthDate:yyyy-MM-dd})";
}

为什么要有层次?

想象一下,我们有一堆用户,要按字母顺序输出:先按姓,再按名。如果姓和名都一样——再按出生日期。

比较逻辑:"责任链"

这很像奥赛排名:先按分数,分数一样再按交卷时间,还一样就按字母顺序。代码里实现其实很简单:


public class UserComparer : IComparer<User>
{
    public int Compare(User x, User y)
    {
        // 比较姓
        int result = string.Compare(x.LastName, y.LastName, StringComparison.OrdinalIgnoreCase);

        if (result != 0) return result; // 不一样就直接返回

        // 姓一样——比名
        result = string.Compare(x.FirstName, y.FirstName, StringComparison.OrdinalIgnoreCase);
        if (result != 0) return result;

        // 姓名都一样——比出生日期
        return x.BirthDate.CompareTo(y.BirthDate);
    }
}

怎么用:


var users = new List<User>
{
    new User { FirstName = "伊万", LastName = "伊万诺夫", BirthDate = new DateTime(1990, 1, 1) },
    new User { FirstName = "彼得", LastName = "伊万诺夫", BirthDate = new DateTime(1992, 5, 1) },
    new User { FirstName = "安娜", LastName = "彼得罗娃", BirthDate = new DateTime(1985, 8, 30) }
};

users.Sort(new UserComparer());
users.ForEach(Console.WriteLine);
// 彼得罗娃 安娜 (1985-08-30)
// 伊万诺夫 伊万 (1990-01-01)
// 伊万诺夫 彼得 (1992-05-01)

2. 字符串怎么比?文化差异

字符串比较:Ordinal、CurrentCulture、InvariantCulture

字符串嘛,看起来都一样,其实没那么简单!比如俄语的ё和е,德语的ss和ß,还有大小写……

.NET里字符串比较有专门的规则,通过StringComparison指定。这会影响排序和查找。

字符串比较例子:


// 在德语里 ß 基本等于 ss
string a = "straße";
string b = "STRASSE";

bool eq1 = string.Equals(a, b, StringComparison.Ordinal); // false
bool eq2 = string.Equals(a, b, StringComparison.OrdinalIgnoreCase); // false
bool eq3 = string.Equals(a, b, StringComparison.CurrentCultureIgnoreCase); // true 

前两个是按字节比——不管文化和语言,所以ß和SS不一样。第三个用的是当前文化(比如德语),字符串就按母语者的习惯理解:ß当成ss,大小写也不管。所以eq3会返回true。

怎么选对的比较方式?

  • Ordinal——快,按字节,适合技术场景(比如比ID)。
  • CurrentCulture / InvariantCulture——用户文本,按操作系统或指定文化的规则。

在Sort、Compare等方法里,最好明确指定比较方式:


string.Compare(x, y, StringComparison.CurrentCultureIgnoreCase)

不同语言下的排序特点

俄语排序——ё有时排在е后面,有时算一样(看文化!)。所以如果做严肃的东西(比如字典),最好问清楚客户或业务分析师,特殊字母到底怎么排。

3. null保护:不是所有对象都乖乖的

如果字段是null怎么办?

实际开发里,总有人忘了填字段,这时候一比较——砰!NullReferenceException异常。咱们得防着点。

没做null保护的例子:


public int Compare(User x, User y)
{
    return x.LastName.CompareTo(y.LastName); // 如果LastName==null,会报错!
}

加了保护的例子(null比任何非null都小):


public int Compare(User x, User y)
{
    // 用专门的字符串比较器,能处理null
    int byLastName = Comparer<string>.Default.Compare(x.LastName, y.LastName);
    if (byLastName != 0) return byLastName;

    // 以此类推...
}

还可以更简洁:


public int Compare(User x, User y)
{
    return string.Compare(x?.LastName, y?.LastName, StringComparison.OrdinalIgnoreCase);
}

"Null"在前还是在后?

可以让所有"空"姓的用户排在前面或后面——看需求。


public int Compare(User x, User y)
{
    if (x.LastName == null && y.LastName == null) return 0;
    if (x.LastName == null) return 1;   // null排最后
    if (y.LastName == null) return -1;  // null排最后
    return string.Compare(x.LastName, y.LastName, StringComparison.OrdinalIgnoreCase);
}

4. 多条件比较

新手常犯的错——用算术代替"责任链",比如:


// 千万别这样!
public int Compare(User x, User y)
{
    // 坏例子
    return (x.Age - y.Age) + string.Compare(x.FirstName, y.FirstName, StringComparison.Ordinal);
}

这种写法不能保证排序正确:如果年龄差-100,字符串比较返回1,结果就是-99,这和预期逻辑不符。

正确做法是明确顺序:
有差异就直接返回,否则看下一个条件。

5. 有用的小细节

如果对象主要条件都一样怎么办?

如果对象真的"一样",排序算法最好是稳定的:不会改变那些比较结果相等元素的顺序。内置的List<T>.Sort()不保证稳定。如果很重要(比如多级用户排序的表格),用LINQ的OrderBy/ThenBy——它们是稳定的。

带可选/nullable字段的比较

.NET里经常有模型字段是DateTime?(Nullable<DateTime>)或int?。逻辑很简单:null比非null小,或者反过来——看需求。可以用标准库的辅助方法:


int result = Nullable.Compare<DateTime>(u1.BirthDate, u2.BirthDate);

带额外规则的比较

有时候要让某个条件有"权重",比如VIP客户永远排最前。做法——把"VIP标志"加到排序最前面。


public int Compare(User x, User y)
{
    // VIP排前面
    int vipResult = y.IsVip.CompareTo(x.IsVip); // true=1, false=0;降序
    if (vipResult != 0) return vipResult;

    // 其他照常
    int result = string.Compare(x.LastName, y.LastName, StringComparison.OrdinalIgnoreCase);
    if (result != 0) return result;
    return string.Compare(x.FirstName, y.FirstName, StringComparison.OrdinalIgnoreCase);
}

6. 建议和最佳实践

一定要检查null
现代C#越来越追求"null安全",但老代码可不一定。一定要加保护,或者用合适的方法。

别用"魔法数字"
不要写return x.Field - y.Field;,尤其是可能溢出的字段(overflow)。如果是long类型——肯定有坑。

用StringComparison
别指望字符串默认比较。一定要明确传StringComparison.OrdinalIgnoreCase或其他合适的。

区分比较和相等
IComparable<T>和IEqualityComparer<T>是两回事。排序用比较器,查唯一用等价器。有时候结果还真不一样!

给"非典型"情况加测试
检查下排序在字段相等、字段为空、字符串大小写或语言不同的情况下是不是对的。

2
任务
C# SELF, 第 30 级, 课程 3
已锁定
考虑文化差异的字符串比较
考虑文化差异的字符串比较
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION