From 0b7ff155f898e398ef4a3ff405a3c0ca6b17f6a6 Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sun, 8 Nov 2026 18:33:17 +0100 Subject: [PATCH] ObjectComparison: .NET 10, null-safe comparers, copy-per-call benchmark Targets net10.0 with BenchmarkDotNet 0.15.8, xunit 2.9.3, xunit.runner.visualstudio 4.0.1, Microsoft.NET.Test.Sdk 18.11.0 and coverlet.collector 10.1.0. The comparers and Comparison delegate methods now sort nulls first instead of returning 0, the benchmark copies a shuffled source before each sort, Program.cs adds a lambda sort by Name and passes args to BenchmarkRunner, and the tests cover descending order, the lambda, nulls, random input and SortedSet. --- .../ObjectComparison/Employee.cs | 77 ++++---- .../ObjectComparisonBenchmark.cs | 89 ++++----- .../ObjectComparison/ObjectComparisons.csproj | 4 +- .../ObjectComparison/Program.cs | 10 +- .../SortByIdAscendingHelper.cs | 19 +- .../SortByIdDescendingHelper.cs | 19 +- .../Tests/Tests/EmployeeTests.cs | 179 ++++++++++++------ .../ObjectComparison/Tests/Tests/Tests.csproj | 10 +- 8 files changed, 251 insertions(+), 156 deletions(-) diff --git a/csharp-intermediate-topics/ObjectComparison/ObjectComparison/Employee.cs b/csharp-intermediate-topics/ObjectComparison/ObjectComparison/Employee.cs index 84aa0039da..15262d8b11 100644 --- a/csharp-intermediate-topics/ObjectComparison/ObjectComparison/Employee.cs +++ b/csharp-intermediate-topics/ObjectComparison/ObjectComparison/Employee.cs @@ -1,55 +1,58 @@ -namespace ObjectComparisons +namespace ObjectComparisons; + +public partial class Employee : IComparable { - public partial class Employee : IComparable + public int Id { get; set; } + public string? Name { get; set; } + + public Employee(int id, string? name) { - public int Id { get; set; } - public string? Name { get; set; } + Id = id; + Name = name; + } - public Employee(int id, string? name) - { - Id = id; - Name = name; - } + public int CompareTo(Employee? otherEmployee) + { + return otherEmployee is null ? 1 : Id.CompareTo(otherEmployee.Id); + } - public int CompareTo(Employee? otherEmployee) - { - return otherEmployee is null ? 1 : Id.CompareTo(otherEmployee.Id); - } + public static IComparer SortByIdAscending() + { + return new SortByIdAscendingHelper(); + } - public static IComparer SortByIdAscending() - { - return new SortByIdAscendingHelper(); - } + public static IComparer SortByIdDescending() + { + return new SortByIdDescendingHelper(); + } - public static IComparer SortByIdDescending() + public static int CompareEmployeesByIdAscending(Employee? employee1, Employee? employee2) + { + if (employee1 is null) { - return new SortByIdDescendingHelper(); + return employee2 is null ? 0 : -1; } - public static int CompareEmployeesByIdAscending(Employee employee1, Employee employee2) + if (employee2 is null) { - if ((employee1 is null) || (employee2 is null)) - { - return 0; - } + return 1; + } - int value1 = employee1?.Id ?? 0; - int value2 = employee2?.Id ?? 0; + return employee1.Id.CompareTo(employee2.Id); + } - return value1.CompareTo(value2); + public static int CompareEmployeesByIdDescending(Employee? employee1, Employee? employee2) + { + if (employee1 is null) + { + return employee2 is null ? 0 : -1; } - public static int CompareEmployeesByIdDescending(Employee employee1, Employee employee2) + if (employee2 is null) { - if ((employee1 is null) || (employee2 is null)) - { - return 0; - } - - int value1 = employee1?.Id ?? 0; - int value2 = employee2?.Id ?? 0; - - return value2.CompareTo(value1); + return 1; } + + return employee2.Id.CompareTo(employee1.Id); } } diff --git a/csharp-intermediate-topics/ObjectComparison/ObjectComparison/ObjectComparisonBenchmark.cs b/csharp-intermediate-topics/ObjectComparison/ObjectComparison/ObjectComparisonBenchmark.cs index f9fcc75d19..9ff64a48cc 100644 --- a/csharp-intermediate-topics/ObjectComparison/ObjectComparison/ObjectComparisonBenchmark.cs +++ b/csharp-intermediate-topics/ObjectComparison/ObjectComparison/ObjectComparisonBenchmark.cs @@ -2,50 +2,53 @@ using BenchmarkDotNet.Configs; using BenchmarkDotNet.Order; -namespace ObjectComparisons +namespace ObjectComparisons; + +[MemoryDiagnoser] +[Orderer(SummaryOrderPolicy.FastestToSlowest)] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByParams)] +public class ObjectComparisonBenchmark { - [MemoryDiagnoser] - [Orderer(SummaryOrderPolicy.FastestToSlowest)] - [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByParams)] - public class ObjectComparisonBenchmark + private Employee[] _source = []; + private Employee[] _employees = []; + + [Params(5, 1_000)] + public int Size { get; set; } + + [GlobalSetup] + public void Setup() + { + var ids = Enumerable.Range(1, Size).ToArray(); + new Random(42).Shuffle(ids); + + _source = ids.Select(id => new Employee(id, $"Employee {id}")).ToArray(); + _employees = new Employee[Size]; + } + + [Benchmark] + public Employee[] CompareUsingIComparable() + { + _source.CopyTo(_employees, 0); + Array.Sort(_employees); + + return _employees; + } + + [Benchmark] + public Employee[] CompareUsingIComparer() + { + _source.CopyTo(_employees, 0); + Array.Sort(_employees, Employee.SortByIdAscending()); + + return _employees; + } + + [Benchmark] + public Employee[] CompareUsingComparisonDelegate() { - [Params(10000)] - public int Count { get; set; } - - public Employee[] Employees { get; } = new Employee[5] - { - new Employee(4, "John"), - new Employee(2, "Tom"), - new Employee(1, "Eric"), - new Employee(5, "Dan"), - new Employee(3, "Alen") - }; - - [Benchmark] - public void CompareUsingIComparable() - { - for (int i = 0; i < Count; i++) - { - Array.Sort(Employees); - } - } - - [Benchmark] - public void CompareUsingIComparer() - { - for (int i = 0; i < Count; i++) - { - Array.Sort(Employees, Employee.SortByIdAscending()); - } - } - - [Benchmark] - public void CompareUsingComparisonDelegate() - { - for (int i = 0; i < Count; i++) - { - Array.Sort(Employees, Employee.CompareEmployeesByIdAscending); - } - } + _source.CopyTo(_employees, 0); + Array.Sort(_employees, Employee.CompareEmployeesByIdAscending); + + return _employees; } } diff --git a/csharp-intermediate-topics/ObjectComparison/ObjectComparison/ObjectComparisons.csproj b/csharp-intermediate-topics/ObjectComparison/ObjectComparison/ObjectComparisons.csproj index a6d8996d10..62be4f867f 100644 --- a/csharp-intermediate-topics/ObjectComparison/ObjectComparison/ObjectComparisons.csproj +++ b/csharp-intermediate-topics/ObjectComparison/ObjectComparison/ObjectComparisons.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net10.0 enable enable - + diff --git a/csharp-intermediate-topics/ObjectComparison/ObjectComparison/Program.cs b/csharp-intermediate-topics/ObjectComparison/ObjectComparison/Program.cs index eeec61736a..c0dde4625c 100644 --- a/csharp-intermediate-topics/ObjectComparison/ObjectComparison/Program.cs +++ b/csharp-intermediate-topics/ObjectComparison/ObjectComparison/Program.cs @@ -53,7 +53,13 @@ PrintEmployeeList(employeeList); -BenchmarkRunner.Run(); +employeeList.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal)); + +Console.WriteLine($"{Environment.NewLine}Sorting by Name using a Lambda Expression"); + +PrintEmployeeList(employeeList); + +BenchmarkRunner.Run(args: args); static void PrintEmployees(Employee[] employees) { @@ -69,4 +75,4 @@ static void PrintEmployeeList(List employees) { Console.WriteLine(employee.Id + "\t\t" + employee.Name); } -} \ No newline at end of file +} diff --git a/csharp-intermediate-topics/ObjectComparison/ObjectComparison/SortByIdAscendingHelper.cs b/csharp-intermediate-topics/ObjectComparison/ObjectComparison/SortByIdAscendingHelper.cs index 3a0cc14719..bf19ea50cc 100644 --- a/csharp-intermediate-topics/ObjectComparison/ObjectComparison/SortByIdAscendingHelper.cs +++ b/csharp-intermediate-topics/ObjectComparison/ObjectComparison/SortByIdAscendingHelper.cs @@ -1,12 +1,19 @@ -namespace ObjectComparisons +namespace ObjectComparisons; + +public class SortByIdAscendingHelper : IComparer { - public class SortByIdAscendingHelper : IComparer + public int Compare(Employee? firstEmployee, Employee? secondEmployee) { - public int Compare(Employee? firstEmployee, Employee? secondEmployee) + if (firstEmployee is null) { - return ((firstEmployee is null) || (secondEmployee is null)) ? - 0 : - firstEmployee.Id.CompareTo(secondEmployee.Id); + return secondEmployee is null ? 0 : -1; } + + if (secondEmployee is null) + { + return 1; + } + + return firstEmployee.Id.CompareTo(secondEmployee.Id); } } diff --git a/csharp-intermediate-topics/ObjectComparison/ObjectComparison/SortByIdDescendingHelper.cs b/csharp-intermediate-topics/ObjectComparison/ObjectComparison/SortByIdDescendingHelper.cs index 889c3b2b11..5e1d8bb916 100644 --- a/csharp-intermediate-topics/ObjectComparison/ObjectComparison/SortByIdDescendingHelper.cs +++ b/csharp-intermediate-topics/ObjectComparison/ObjectComparison/SortByIdDescendingHelper.cs @@ -1,12 +1,19 @@ -namespace ObjectComparisons +namespace ObjectComparisons; + +public class SortByIdDescendingHelper : IComparer { - public class SortByIdDescendingHelper : IComparer + public int Compare(Employee? firstEmployee, Employee? secondEmployee) { - public int Compare(Employee? firstEmployee, Employee? secondEmployee) + if (firstEmployee is null) { - return ((firstEmployee is null) || (secondEmployee is null)) ? - 0 : - secondEmployee.Id.CompareTo(firstEmployee.Id); + return secondEmployee is null ? 0 : -1; } + + if (secondEmployee is null) + { + return 1; + } + + return secondEmployee.Id.CompareTo(firstEmployee.Id); } } diff --git a/csharp-intermediate-topics/ObjectComparison/Tests/Tests/EmployeeTests.cs b/csharp-intermediate-topics/ObjectComparison/Tests/Tests/EmployeeTests.cs index 6ccbd091f1..a6df7d8886 100644 --- a/csharp-intermediate-topics/ObjectComparison/Tests/Tests/EmployeeTests.cs +++ b/csharp-intermediate-topics/ObjectComparison/Tests/Tests/EmployeeTests.cs @@ -1,59 +1,128 @@ using ObjectComparisons; using Xunit; -namespace Tests +namespace Tests; + +public class EmployeeTests { - public class EmployeeTests - { - [Fact] - public void EmployeeArray_WhenDefaultSorted_ThenIsOrderedBasedOnId() - { - var employees = GetEmployees(); - - Array.Sort(employees); - - for (int i = 0; i < 4; i++) - { - Assert.True(employees[i].Id < employees[i + 1].Id); - } - } - - [Fact] - public void EmployeeArray_WhenSortedUsingIComparer_ThenIsOrderedBasedOnId() - { - var employees = GetEmployees(); - - Array.Sort(employees, Employee.SortByIdAscending()); - - for (int i = 0; i < 4; i++) - { - Assert.True(employees[i].Id < employees[i + 1].Id); - } - } - - [Fact] - public void EmployeeArray_WhenSortedUsingComparisonDelegate_ThenIsOrderedBasedOnId() - { - var employees = GetEmployees(); - - Array.Sort(employees, Employee.CompareEmployeesByIdAscending); - - for (int i = 0; i < 4; i++) - { - Assert.True(employees[i].Id < employees[i + 1].Id); - } - } - - private static Employee[] GetEmployees() - { - return new Employee[5] - { - new Employee(4, "John"), - new Employee(2, "Tom"), - new Employee(1, "Eric"), - new Employee(5, "Dan"), - new Employee(3, "Alen") - }; - } - } -} \ No newline at end of file + [Fact] + public void EmployeeArray_WhenDefaultSorted_ThenIsOrderedBasedOnId() + { + var employees = GetEmployees(); + + Array.Sort(employees); + + Assert.Equal([1, 2, 3, 4, 5], employees.Select(e => e.Id)); + } + + [Fact] + public void EmployeeArray_WhenSortedUsingIComparer_ThenIsOrderedBasedOnId() + { + var employees = GetEmployees(); + + Array.Sort(employees, Employee.SortByIdAscending()); + + Assert.Equal([1, 2, 3, 4, 5], employees.Select(e => e.Id)); + } + + [Fact] + public void EmployeeArray_WhenSortedUsingDescendingIComparer_ThenIsOrderedBasedOnIdDescending() + { + var employees = GetEmployees(); + + Array.Sort(employees, Employee.SortByIdDescending()); + + Assert.Equal([5, 4, 3, 2, 1], employees.Select(e => e.Id)); + } + + [Fact] + public void EmployeeArray_WhenSortedUsingComparisonDelegate_ThenIsOrderedBasedOnId() + { + var employees = GetEmployees(); + + Array.Sort(employees, Employee.CompareEmployeesByIdAscending); + + Assert.Equal([1, 2, 3, 4, 5], employees.Select(e => e.Id)); + } + + [Fact] + public void EmployeeList_WhenSortedUsingLambda_ThenIsOrderedBasedOnName() + { + var employees = GetEmployees().ToList(); + + employees.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal)); + + Assert.Equal(["Alen", "Dan", "Eric", "John", "Tom"], employees.Select(e => e.Name)); + } + + [Fact] + public void EmployeeArrayWithNulls_WhenSortedByEveryApproach_ThenNullsComeFirstAndIdsAreOrdered() + { + Employee?[] expected = [null, null, .. GetEmployees().OrderBy(e => e.Id)]; + + Employee?[] byIComparable = GetEmployeesWithNulls(); + Array.Sort(byIComparable); + + Employee?[] byIComparer = GetEmployeesWithNulls(); + Array.Sort(byIComparer, Comparer.Create(new SortByIdAscendingHelper().Compare)); + + Employee?[] byDelegate = GetEmployeesWithNulls(); + Array.Sort(byDelegate, Employee.CompareEmployeesByIdAscending); + + Assert.Equal(expected.Select(e => e?.Id), byIComparable.Select(e => e?.Id)); + Assert.Equal(expected.Select(e => e?.Id), byIComparer.Select(e => e?.Id)); + Assert.Equal(expected.Select(e => e?.Id), byDelegate.Select(e => e?.Id)); + } + + [Fact] + public void RandomEmployees_WhenSortedByEveryApproach_ThenMatchLinqOrderBy() + { + var random = new Random(7); + var employees = Enumerable.Range(0, 500) + .Select(i => new Employee(random.Next(1_000), $"Employee {i}")) + .ToArray(); + var expected = employees.Select(e => e.Id).Order().ToArray(); + + var byIComparable = (Employee[])employees.Clone(); + Array.Sort(byIComparable); + + var byIComparer = (Employee[])employees.Clone(); + Array.Sort(byIComparer, Employee.SortByIdAscending()); + + var byDelegate = (Employee[])employees.Clone(); + Array.Sort(byDelegate, Employee.CompareEmployeesByIdAscending); + + Assert.Equal(expected, byIComparable.Select(e => e.Id)); + Assert.Equal(expected, byIComparer.Select(e => e.Id)); + Assert.Equal(expected, byDelegate.Select(e => e.Id)); + } + + [Fact] + public void SortedSet_WhenGivenComparerCreatedFromLambda_ThenIsOrderedBasedOnName() + { + var employeesByName = new SortedSet( + GetEmployees(), + Comparer.Create((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal))); + + Assert.Equal(["Alen", "Dan", "Eric", "John", "Tom"], employeesByName.Select(e => e.Name)); + } + + private static Employee[] GetEmployees() + { + return + [ + new Employee(4, "John"), + new Employee(2, "Tom"), + new Employee(1, "Eric"), + new Employee(5, "Dan"), + new Employee(3, "Alen") + ]; + } + + private static Employee?[] GetEmployeesWithNulls() + { + var employees = GetEmployees(); + + return [employees[0], null, employees[1], employees[2], null, employees[3], employees[4]]; + } +} diff --git a/csharp-intermediate-topics/ObjectComparison/Tests/Tests/Tests.csproj b/csharp-intermediate-topics/ObjectComparison/Tests/Tests/Tests.csproj index 1f137c7bcd..146a5a6269 100644 --- a/csharp-intermediate-topics/ObjectComparison/Tests/Tests/Tests.csproj +++ b/csharp-intermediate-topics/ObjectComparison/Tests/Tests/Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net10.0 enable enable @@ -9,13 +9,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all