From 76d802adb1eefd21d7a8a7d5350c9e6d082b4668 Mon Sep 17 00:00:00 2001 From: foundation-worker Date: Wed, 29 Jul 2026 20:32:22 +0800 Subject: [PATCH] =?UTF-8?q?refactor(extensions):=20=E9=99=8D=E4=BD=8E=20Se?= =?UTF-8?q?quenceEqual=20=E8=AE=A4=E7=9F=A5=E5=A4=8D=E6=9D=82?= =?UTF-8?q?=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 提取 EqualAsCollection 与 EqualByEnumerator 两个私有助手,把 ICollection / IList / IEnumerator 三条路径下沉,让公有 SequenceEqual 方法的认知复杂度由 18 降到 ≤15,关闭 Sonar S3776(csharpsquid:S3776, CRITICAL/CODE_SMELL)。公开签名、参数校验、委托语义与原有行为均保持不变;补充 4 条 [Fact] 覆盖 LengthMismatch / PredicateFailure / CollectionNotList / LazyEnumerable 路径。 Linear: GFX-196 --- .../IEnumerableExtensions.cs | 65 +++++++++++++++---- .../Extensions/IEnumerableExtensionsTests.cs | 62 ++++++++++++++++++ 2 files changed, 114 insertions(+), 13 deletions(-) diff --git a/GameFrameX.Foundation.Extensions/IEnumerableExtensions.cs b/GameFrameX.Foundation.Extensions/IEnumerableExtensions.cs index 2ed74a1..6736111 100644 --- a/GameFrameX.Foundation.Extensions/IEnumerableExtensions.cs +++ b/GameFrameX.Foundation.Extensions/IEnumerableExtensions.cs @@ -1283,26 +1283,65 @@ public static bool SequenceEqual(this IEnumerable first, IEnumerable if (first is ICollection source1 && second is ICollection source2) { - if (source1.Count != source2.Count) - { - return false; - } + return EqualAsCollection(source1, source2, condition); + } - if (source1 is IList list1 && source2 is IList list2) + return EqualByEnumerator(first, second, condition); + } + + /// + /// 当双方都已实现 时走集合快路径:长度不一致直接 false;只要一方同时实现 就以 索引方式逐元素比较,否则回退到枚举器比较。 + /// + /// + /// Compares two with a custom condition. + /// Returns false immediately when the counts differ; + /// if both sides also implement the indexed access path is used, + /// otherwise the enumerator fallback is used. + /// Splitting this branch out of keeps that method's cognitive complexity within Sonar S3776's 15. + /// + /// 第一个序列 / The first sequence + /// 第二个序列 / The second sequence + /// 用于比较两个元素的条件 / The condition to compare two elements + /// 长度相等且所有对应元素满足条件时返回 true / Returns true if lengths match and all pairs satisfy + private static bool EqualAsCollection(ICollection first, ICollection second, Func condition) + { + if (first.Count != second.Count) + { + return false; + } + + if (first is IList list1 && second is IList list2) + { + var count = list1.Count; + for (var index = 0; index < count; ++index) { - var count = source1.Count; - for (var index = 0; index < count; ++index) + if (!condition(list1[index], list2[index])) { - if (!condition(list1[index], list2[index])) - { - return false; - } + return false; } - - return true; } + + return true; } + return EqualByEnumerator(first, second, condition); + } + + /// + /// 通过并行枚举两个 序列并以 逐元素比较,确认二者长度相等且每个对应位置的元素都满足条件。 + /// + /// + /// Compares two sequences element-by-element via parallel enumeration. + /// Used by when no fast path is available, + /// and by the collection fast path when at least one side is not an . + /// Splitting this branch out of keeps that method's cognitive complexity within Sonar S3776's 15. + /// + /// 第一个序列 / The first sequence + /// 第二个序列 / The second sequence + /// 用于比较两个元素的条件 / The condition to compare two elements + /// 长度相等且所有对应元素满足条件时返回 true / Returns true if lengths match and all pairs satisfy + private static bool EqualByEnumerator(IEnumerable first, IEnumerable second, Func condition) + { using var enumerator1 = first.GetEnumerator(); using var enumerator2 = second.GetEnumerator(); while (enumerator1.MoveNext()) diff --git a/GameFrameX.Foundation.Tests/Extensions/IEnumerableExtensionsTests.cs b/GameFrameX.Foundation.Tests/Extensions/IEnumerableExtensionsTests.cs index 5af1fb2..6bff2cc 100644 --- a/GameFrameX.Foundation.Tests/Extensions/IEnumerableExtensionsTests.cs +++ b/GameFrameX.Foundation.Tests/Extensions/IEnumerableExtensionsTests.cs @@ -361,6 +361,68 @@ public void SequenceEqual_DifferentTypes_ShouldReturnTrue() Assert.True(result); } + + [Fact] + public void SequenceEqual_ICollectionLengthMismatch_ShouldReturnFalse() + { + // Arrange: both sides are IList (also ICollection) with differing lengths. + var first = new[] { 1, 2, 3 }; + var second = new[] { 1, 2 }; + + // Act + var result = first.SequenceEqual(second, (x, y) => x == y); + + // Assert + Assert.False(result); + } + + [Fact] + public void SequenceEqual_PredicateFailure_ShouldReturnFalse() + { + // Arrange: identical lengths but predicate rejects the second element. + var first = new[] { 1, 2, 3 }; + var second = new[] { 1, 9, 3 }; + + // Act + var result = first.SequenceEqual(second, (x, y) => x == y); + + // Assert + Assert.False(result); + } + + [Fact] + public void SequenceEqual_CollectionNotList_ShouldReturnTrue() + { + // Arrange: LinkedList implements ICollection but not IList, so the + // helper must fall back to the enumerator path while still returning true. + var first = new LinkedList(new[] { 1, 2, 3 }); + var second = new LinkedList(new[] { 1, 2, 3 }); + + // Act + var result = first.SequenceEqual(second, (x, y) => x == y); + + // Assert + Assert.True(result); + } + + [Fact] + public void SequenceEqual_LazyEnumerable_ShouldReturnTrue() + { + // Arrange: pure IEnumerable with no collection optimisation available. + static IEnumerable Range(int from, int count) + { + for (var i = 0; i < count; i++) + { + yield return from + i; + } + } + + // Act + var result = Range(10, 3).SequenceEqual(Range(10, 3), (x, y) => x == y); + + // Assert + Assert.True(result); + } #endregion #region CompareChanges Tests