0

Imagine I have an IEnumerable<T> where T is a struct type. For each element of the collection I want to check whether there is another element with the same value.

Firstly, I thought about something like this:

IEnumerable<T> collection = someInput;

foreach(var element in collection)
{
  try
  {
    collection.First(x => x == element &&
                          x.GetHashCode() =! element.GetHashCode());

    DoA(element);
  }
  catch
  {
    DoB(element);
  }
}

But then I found out that hashes are actually equal for structures having same values. Apparently, Object.ReferenceEquals(x, element) is not a way as well.

So, there are 2 questions:

  1. Is there an option to distinguish two different struct variables with the same values?
  2. Is there any other other ways to solve my problem?

Thanks

Alex
  • 124
  • 8
  • 6
    `I found out that hashes are actually equal for structures having same values` - which is not surprising as it is the [very purpose of their existence](https://learn.microsoft.com/en-us/dotnet/api/system.object.gethashcode?view=net-7.0#remarks). – GSerg Jan 23 '23 at 22:31
  • 1
    *Is there an option to distinguish two different struct variables with the same values?* -- what do you mean by this? `struct` values exist on the stack, not the heap, so when you assign one, you **copy it**. Thus there is no notion of reference equality for structs. With this in mind, what are you trying to accomplish? Please [edit] your question to clarify. – dbc Jan 23 '23 at 22:35
  • 1
    Does this answer your question? [C# LINQ find duplicates in List](https://stackoverflow.com/questions/18547354/c-sharp-linq-find-duplicates-in-list) – GSerg Jan 23 '23 at 22:35
  • You cannot use == operator on struct type. You need to use `element.Equals(value)` operator, ideally `where T : IEquatable`. – Adam Jan 23 '23 at 22:43
  • 1
    @Adam So you're saying `if (x == 5)` is not possible and `if (x.Equals(5))` must be used instead? – GSerg Jan 23 '23 at 22:48
  • @GSerg With `T : struct`, variable `T x` cannot be compared to 5 using `x == 5`. It will generate a compile error. You would need to use the Equals operator, but without `T : IEquatable`, the result may not be what you expect. – Adam Jan 23 '23 at 22:55
  • @Adam [`int` is a struct](https://github.com/microsoft/referencesource/blob/master/mscorlib/system/int32.cs). So is e.g. [`Point`](https://learn.microsoft.com/en-us/dotnet/api/system.drawing.point?view=net-7.0), are you saying that [comparing them with `==`](https://dotnetfiddle.net/3p8GKJ) is also impossible? – GSerg Jan 23 '23 at 23:00
  • @GSerg You are confusing direction. Yes, `int` and `Point` are struct, but that does not mean a struct is int or Point. If you define `int x` you can use `x == 5`, but if you use `T x' where `T : struct` you cannot use `x == 5`. – Adam Jan 23 '23 at 23:06
  • 1
    @Adam it is plausible that OP indeed limited the generic method properly to allow for such comparison with `where T: struct, IEqualityOperators`. (Whether it is likely the case or not is open for debate, I'd expect one knowing about these interfaces less likely to have problem with value types in general) – Alexei Levenkov Jan 24 '23 at 01:42
  • @Adam - Do you mean something like this: https://dotnetfiddle.net/FYWqKq ? – Enigmativity Jan 24 '23 at 05:49
  • 1
    @Enigmativity If OP were using `MyStruct` as their structure (or any other defined structure like it, such as int) then they could use == operator. If they are using pure generics (ex: where T : struct) they could not as the C# runtime does not define default == operator for struct type. – Adam Jan 24 '23 at 15:45
  • Thank you all for your points. In my case, I have an array of ```char```, where ```==``` operator is defined. I didn't expect that my generalization from ```char``` to ```struct``` may cause some misunderstanding since ```==``` is not predefined for user's structs. – Alex Jan 24 '23 at 16:31

2 Answers2

3

Is there an option to distinguish two different struct variables with the same values?

No, structs are so called value types. They are only defined by their values and have no reference. If you want to distinguish two instances which have equal values you have to use a class instead of a struct. Classes are reference types and therefore are distinguishable even if they have equal values because they have different references.

In this case however you also have their position in the collection which could be used to distinguish them (it's bascially also some kind of reference).

Is there any other other ways to solve my problem?

As noted above you may use the position. Here's a simple basic implementation without LINQ. You can certainly make a shorter one with LINQ.

    for (var i = 0; i < collection.Count; i++)
    {
        var foundEqual = false;
        for (var j = 0; j < collection.Count; j++)
        {
            if (j != i && collection[i] == collection[j])
            {
                foundEqual = true;
                break;
            }
        }
        
        if (foundEqual)
        {
            DoA(collection[i]);
        }
        else
        {
            DoB(collection[i]);
        }   
    }

If your struct doesn't implement IEquatable yet you have to implement it to make the equality comparison work. Look here for an explanation and an example: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/how-to-define-value-equality-for-a-type

You should never rely on GetHashCode() for equality comparison as an equal hash code does not guarantee that the compared objects are actually equal.

  • So you are saying that if an array of ints happens to contain two equal ints, there is no way to tell they are in different positions in the array? – GSerg Jan 23 '23 at 22:36
  • Good point. I was looking at the problem in a more general way. Of course you could still distinguish by position in this particular scenario. Good point. Will edit! – UnrealSoftware Jan 23 '23 at 22:40
  • 1
    Now your code implies that equality of hash codes means equality of objects; that is [not true](https://learn.microsoft.com/en-us/dotnet/api/system.object.gethashcode?view=net-7.0#remarks). – GSerg Jan 23 '23 at 23:04
  • Thanks again. You are totally right. Adjusted the answer! – UnrealSoftware Jan 23 '23 at 23:12
  • Thank you for your answer! I am only curious about ```var``` using in your code instead of explicit ```int```/```bool``` type declaring. What is the point here? Does it make your code more clear in some way? – Alex Jan 24 '23 at 16:09
  • Glad to hear that it helped! Using var is good practice in general because it makes it easier to change code. In this case you might want to switch "collection" with another class which returns a long for the Count property. That would just work without any extra changes as "i" and "j" will automatically become longs as well. – UnrealSoftware Jan 25 '23 at 19:17
0

Structs will autmatically be compared to each other by a hashcode generated from their fields, so you should be able to use the Equals method to determine if there are any duplicates in your collection. One simple way to do this is to group your collection by the struct itself, and then compare the counts of the grouped items and the original collection. If there were any identical items, they would be grouped together, and the grouped item count would be less than the original collection count:

bool hasDuplicates = collection.GroupBy(i => i).Count() < collection.Count();

In general, you can compare items using the Equals method:

public bool HasDuplicateElement<T>(IEnumerable<T> items) where T : struct
{
    if (items == null || items.Count() < 2) return false;

    for(int i = 0; i < items.Count() - 1; i++)
    {
        for (int j = i + 1; j < items.Count(); j++)
        {
            if (items.ElementAt(i).Equals(items.ElementAt(j))) return true;
        }
    }

    return false;
}

Note that if the struct has reference-type fields, and they don't have a meaningful equality implementation (i.e. they use the default reference equality), then you'll have to write your own IEqualityComparer<T> and pass that to the GroupBy method.

Rufus L
  • 36,127
  • 5
  • 30
  • 43