C# LINQ SequenceEqual: Compare Sequences Correctly
c# linq sequenceequal: Learn how SequenceEqual works in C# LINQ, when to use it, its runtime behavior, and how to compare sequences with custom equality.
Comparing two collections for equality is a common requirement, but doing it correctly in C# involves more than checking if both lists have the same length. The c# linq sequenceequal method provides a direct way to determine whether two sequences contain the same elements in the same order. This article explains how SequenceEqual behaves, what it does under the hood, and where it fits in your code.
What SequenceEqual Does
SequenceEqual is a LINQ extension method that returns true if two sequences have identical elements in the same order. It is available on IEnumerable<T> and works with any type that implements that interface, including arrays, lists, and other collections.
int[] first = { 1, 2, 3 }; int[] second = { 1, 2, 3 }; bool areEqual = first.SequenceEqual(second); Console.WriteLine(areEqual); // True
The method compares each element pairwise. If the sequences have different lengths, it returns false immediately without evaluating the remaining elements. This short-circuiting behavior is important for performance when the sequences are large and differ early.
How Equality Is Determined
By default, SequenceEqual uses the default equality comparer for the element type. For value types like int, double, and struct, this means a simple value comparison. For reference types like string or custom classes, it uses EqualityComparer<T>.Default, which calls Equals and GetHashCode.
string[] words1 = { "apple", "banana" }; string[] words2 = { "apple", "banana" }; bool sameWords = words1.SequenceEqual(words2); // True
For custom classes, the default behavior compares references unless the class overrides Equals and GetHashCode. If your class does not override these methods, two distinct instances with identical property values will not be considered equal.
public class Product { public int Id { get; set; } public string Name { get; set; } } Product[] products1 = { new Product { Id = 1, Name = "Laptop" } }; Product[] products2 = { new Product { Id = 1, Name = "Laptop" } }; bool sameProducts = products1.SequenceEqual(products2); // False
To compare based on values, you need to override Equals and GetHashCode in the class or supply a custom comparer, which is covered later.
SequenceEqual vs Manual Comparison
A common alternative is writing a loop to compare elements manually. While a loop gives you full control, SequenceEqual is often more readable and less error-prone.
// Manual comparison bool ManualCompare(int[] a, int[] b) { if (a.Length != b.Length) return false; for (int i = 0; i < a.Length; i++) { if (!a[i].Equals(b[i])) return false; } return true; }
SequenceEqual handles the length check and iteration for you. It also works with any IEnumerable<T>, including lazy sequences, which a manual loop over arrays cannot do directly. However, if you need to compare elements with a custom rule that is not a simple equality check, a manual loop or a custom comparer is necessary.
Performance Considerations
SequenceEqual evaluates the sequences lazily. It pulls elements from both sources as needed, comparing them one at a time. It stops as soon as it finds a mismatch or when one sequence ends. This means it does not create a copy of either sequence and does not allocate additional memory beyond the enumerators themselves.
For large collections that differ early, SequenceEqual can be significantly faster than approaches that materialize both sequences first, such as converting them to arrays or lists and then comparing. The runtime cost is O(n) in the worst case, where n is the length of the shorter sequence, because it must compare every element until a difference is found.
One subtle point: if either sequence is a List<T> or an array, the enumerator is a struct and avoids heap allocation. If the sequence is a yield iterator, the enumerator is a class and will allocate. This allocation is minor but can matter in high-frequency code paths. If you are comparing sequences that are already materialized, the overhead is negligible.
Using a Custom Equality Comparer
When the default equality behavior is not what you need, SequenceEqual has an overload that accepts an IEqualityComparer<T>. This is useful when you want to compare objects by a subset of properties or use a case-insensitive string comparison.
string[] names1 = { "ALICE", "BOB" }; string[] names2 = { "alice", "bob" }; bool caseInsensitiveEqual = names1.SequenceEqual(names2, StringComparer.OrdinalIgnoreCase); Console.WriteLine(caseInsensitiveEqual); // True
For custom classes, you can define a comparer that implements IEqualityComparer<T>.
public class ProductComparer : IEqualityComparer<Product> { public bool Equals(Product x, Product y) { if (x == null || y == null) return x == y; return x.Id == y.Id && x.Name == y.Name; } public int GetHashCode(Product obj) { return HashCode.Combine(obj.Id, obj.Name); } }
Then use it:
Product[] products1 = { new Product { Id = 1, Name = "Laptop" } }; Product[] products2 = { new Product { Id = 1, Name = "Laptop" } }; bool sameProducts = products1.SequenceEqual(products2, new ProductComparer()); // True
Providing a comparer keeps the comparison logic separate from the class itself, which is useful when the equality rule varies by context.
Common Pitfalls and Edge Cases
SequenceEqual is strict about order. {1, 2} and {2, 1} are not equal. If order does not matter, use Except or sort both sequences first, but be aware that sorting changes the original collection unless you copy it.
Null elements are allowed in sequences. If both sequences contain null at the same position, they are considered equal. If one has null and the other does not, they are not equal. The default comparer handles this correctly.
Another edge case is when one sequence is null. SequenceEqual will throw an ArgumentNullException if the first sequence is null, but if the second sequence is null, it will also throw because the method tries to call GetEnumerator on it. Always ensure both sequences are non-null before calling the method.
int[] first = null; int[] second = { 1, 2 }; // Throws ArgumentNullException // bool result = first.SequenceEqual(second);
If you are comparing sequences that are generated lazily, be aware that SequenceEqual will enumerate both sequences completely if they are equal. This can have side effects if the sequences are not pure. For example, if a sequence reads from a file or a network stream, the comparison will consume that data.
When to Use SequenceEqual in Production Code
SequenceEqual is the right tool when you need to verify that two collections contain the same elements in the same order. Common use cases include unit testing assertions, comparing configuration lists, or checking whether a response payload matches an expected result.
In test code, SequenceEqual is often combined with Assert.True or a testing framework's assertion method. In production code, it can be used to decide whether a list has changed and needs to be persisted. For example, you might compare a new set of permissions against the existing set to determine if an update is required.
bool permissionsChanged = !currentPermissions.SequenceEqual(newPermissions); if (permissionsChanged) { // Update database }
One important limitation is that SequenceEqual does not tell you where the sequences differ. If you need to report the index of the first mismatch, you must write a custom loop. For most scenarios, a boolean result is sufficient, but when debugging, a custom comparison that returns the mismatch index can be more helpful.
Another consideration is that SequenceEqual is not suitable for comparing sequences of floating-point numbers where you need tolerance. Because it uses exact equality, 1.0 and 1.0000001 are not equal. In such cases, you need a custom comparer that applies an epsilon tolerance, or you must round the values before comparison.
Finally, remember that SequenceEqual is a LINQ extension method, so it is available in the System.Linq namespace. Ensure you have using System.Linq; in your file. In modern .NET, this is often included implicitly, but in older projects you may need to add it explicitly.