Dylan Beattie
Dylan Beattie

Reputation: 54160

Why can't the operator '==' be applied to a struct and default(struct)?

I'm seeing some odd behaviour after using FirstOrDefault() on a collection of structs. I've isolated it into this reproduction case. This program won't compile

using System;
using System.Linq;

namespace MyProgram {
    public class Program {
        static void Main() {

            var users = new User[] {
            new User() { UserGuid = Guid.NewGuid(), Username = "user01" },
            new User() { UserGuid = Guid.NewGuid(), Username = "user02" }
        };

            var user = users.FirstOrDefault(u => u.Username == "user01");
            Console.WriteLine(user == default(User) ? "not found" : "found");
        }

    }

    public struct User {
        public Guid UserGuid;
        public string Username;
    }
}

The compiler error is the rather cryptic:

Operator '==' cannot be applied to operands of type 'MyProgram.User' and 'MyProgram.User'

Changing the struct to a class works fine - but I'm at a loss as to why I can't compare a struct 'instance' to a default?

Upvotes: 35

Views: 14407

Answers (8)

BionicCode
BionicCode

Reputation: 29028

It's highly recommended to not simply add specialized Equals overloads to your value type.
Always implement IEquatable<T>.

Implementing IEquatable<T> also enables other behavioral improvements as this interface is heavily used by other .NET types like generic collections.
For example, methods like Contains, Remove or IndexOf all rely on IEquatable<T>. Same applies to EqualityComparer<T>.Default references in general (EqualityComparer<T>.Default is also internally used in collections).
If you want your value type to behave properly when used in a common manner (e.g., in collections and tables like Dictionayr<K, V>) you must implement IEquatable<T>.


The default equality operator definition checks for reference equality (compiler level). This means if the instances are the same, or in other words if two variables/operands point to the same memory object, the == operator returns true.

Because value types are always copied by value, the variable will never point to the same memory object (as copying by value means the creatin of a new memory object with a copy of each value of the old memory object).
Equality of a value type is therefore defined by comparing the values of an instance (value equality).
In this context it should be noted that it is not recommended to use object.Equals to compare for equality.
Although ValueType (the base class of e.g., struct or enum) overrides object.Equals to perform a basic value comparison for the passed in value type, object.Equals performs very bad:

  • because the parameter is defined as being of type object there incurs the cost of boxing (conversion of a value type to a reference type).
  • because the equality comparison is as general as possible, the override must use reflection to get all fields - of both participating value types (the current and the instance to compare to) to compare their values one by one.

There is no default equality operator overload for value types. Such an overload must be very slow as it had to use reflection to get all field values of a type for equality comparison (like the override of object.Equals provided by ValueType does). Given the frequency of how often the equality operator is used it makes perfect sense to let each value type define its own specialized equality comparison. This eliminates boxing and the use of reflection.

For this reason, it is recommended best practice to always let your value type implement IEquatable<T> if it qualifies for equality comparison.
This solution also avoids the expensive boxing conversion as the IEquatable<T>.Equals is now able to accept the concrete value type as argument.

When implementing IEquatable<T> it's also best practice to override object.Equals, object.GetHashCode (those overrides always go in tandem) and to overload the equality and inequality operators.
This way consistent behavior is guaranteed across the API.
Note: in the wake of overriding the default object behavior it's also best practice to override object.ToString.

An example implementation of IEquatable<T> to enable all equality comparison features could look as follows:

User.cs

public readonly struct User : IEquatable<User>
{
  public Guid UserGuid { get; }
  public string Username { get; }

  public User(Guid userGuid, string username)
  {
    this.UserGuid = userGuid;
    this.Username = username;
  }

  #region IEquatable<User> implementation

  // Use ValueTuple to simplify the equality comparison implementation
  public bool Equals(User other)
    => (other.UserGuid, other.Username) == (this.UserGuid, this. Username);

  #endregion IEquatable<User> implementation
  
  #region object overrides

  public override bool Equals(object obj)
    => obj is User other && Equals(other);

  public override int GetHashCode()
    => HashCode.Combine(this.UserGuid, this. Username);

  // For the sake of completeness
  public override string ToString()
    => $"User: {this. Username} (ID: {this.UserGuid})";

  #endregion object overrides

  #region Operator overloads

  public static bool operator ==(User left, User right)
    => left.Equals(right);

  public static bool operator !=(User left, User right)
    => !(left == right);

  #endregion Operator overloads
}

Usage example

User user = new User();

// Using the equality operator
bool isDefault = user == default;

// Using the IEquatable.Equals method
bool isDefault = user.Equals(default);

Upvotes: 0

Morse
Morse

Reputation: 9144

Per this Equality of classes and structs in C#

In either case, and in both classes and structs, your implementation should follow the five guarantees of equivalence (for the following rules, assume that x, y and z are not null):

  1. The reflexive property: x.Equals(x) returns true.

  2. The symmetric property: x.Equals(y) returns the same value as y.Equals(x).

  3. The transitive property: if (x.Equals(y) && y.Equals(z)) returns true, then x.Equals(z) returns true.

  4. Successive invocations of x.Equals(y) return the same value as long as the objects referenced by x and y aren't modified.

  5. Any non-null value isn't equal to null. However, x.Equals(y) throws an exception when x is null. That breaks rules 1 or 2, depending on the argument to Equals.

Any struct that you define already has a default implementation of value equality that it inherits from the System.ValueType override of the Object.Equals(Object) method. This implementation uses reflection to examine all the fields and properties in the type. Although this implementation produces correct results, it is relatively slow compared to a custom implementation that you write specifically for the type.

The implementation details for value equality are different for classes and structs. However, both classes and structs require the same basic steps for implementing equality:

  1. Override the virtual Object.Equals(Object) method. In most cases, your implementation of bool Equals( object obj ) should just call into the type-specific Equals method that is the implementation of the System.IEquatable interface. (See step 2.)
  2. Implement the System.IEquatable interface by providing a type-specific Equals method. This is where the actual equivalence comparison is performed. For example, you might decide to define equality by comparing only one or two fields in your type. Don't throw exceptions from Equals. For classes that are related by inheritance:
  • This method should examine only fields that are declared in the class. It should call base.Equals to examine fields that are in the base class. (Don't call base.Equals if the type inherits directly from Object, because the Object implementation of Object.Equals(Object) performs a reference equality check.)
  • Two variables should be deemed equal only if the run-time types of the variables being compared are the same. Also, make sure that the IEquatable implementation of the Equals method for the run-time type is used if the run-time and compile-time types of a variable are different. One strategy for making sure run-time types are always compared correctly is to implement IEquatable only in sealed classes. For more information, see the class example later in this article.
  1. Optional but recommended: Overload the == and != operators.
  2. Override Object.GetHashCode so that two objects that have value equality produce the same hash code.
  3. Optional: To support definitions for "greater than" or "less than," implement the IComparable interface for your type, and also overload the <= and >= operators.

Upvotes: 0

Rjz
Rjz

Reputation: 87

Use:

public static bool IsDefault<TValue>(TValue value) => 
   EqualityComparer<TValue>.Default.Equals(value, default(TValue));

or in C# 7.1+:

public static bool IsDefault<TValue>(TValue value) =>
   EqualityComparer<TValue>.Default.Equals(value, default);

and consider implementing IEquatable<T>.

Explanation
EquityComparer<T>.Default first attempts to use the IEquatable<T> interface gradually whittling down to object.Equals. This both resolves the compiler issue as well as avoid costly reflection based struct member comparisons in cases where IEquatable<T> is implemented.

Caution
Avoid implementing an == operator with the default object.Equals method, because under the hood it will use reflection as well as box your instance depending on how its called. When using EqualityComparer<T>.Default.Equals make sure your struct implements IEquatable<T> otherwise this approach will also result in reflection under the hood.

Details
The == operator is implemented for objects and .Net types, so a custom struct will not have a default == operator implementation.

As a result of this nuance, when compiling generic equality tests, such as:

bool IsDefault<TValue> : where TValue : struct => value == default(TValue)

the compiler cannot determine the IL instruction to generate because the correct equality operator implementation cannot be determined until the generic type is resolved; however, in C# generics are resolved at run-time. So even when you do implement the == operator for a custom struct, you might still run into the issue when generics are involved.

Upvotes: 2

supercat
supercat

Reputation: 81247

In C#, the == token is used to represent two different operators (not all languages use the same token for the two operators; VB.NET uses the tokens = and Is). One of the operators is an overloadable equality test, and is only usable in cases where either an overload is defined for both operand types, or an overload is defined for one operand type and a type to which the other operand is implicitly convertible. The other operator represents a reference-equality test, and is usable in cases where the equality-test operator would be unusable, and where one operand is a class type which derives from the other, one operand is a class type and the other is an interface type, or both operands are interface types.

The first equality-test operator cannot be used with any type (class, interface, or struct) that does not provide an explicit override for it. If the == token is used in cases where the first equality-test operator is not usable, however, C# will try to use the second operator [note that other languages like VB.NET would not do this; in VB.NET, an attempt to to use = to compare two things that don't define an equality-test overload will be an error, even if the things could be compared using the Is operator]. That second operator may be used to compare any reference type to another reference of the same type, but is not usable with structures. Since neither type of equality operator is defined for structures, the comparison is disallowed.

If one is wondering why == doesn't simply fall back upon Equals(Object), which is usable with all types, the reason is that both operands of == are subject to type coercion in ways that would prevent its behavior from matching Equals. For example, 1.0f==1.0, and 1.0==1.0f, both cast the float operand to double, but given an expression like (1.0f).Equals(1.0) the first operand can't be evaluated as anything but float. Further, if == were mapped to Equals, then it would have been necessary for C# to use a different token to represent a reference-equality test [something the language should have done anyway, but apparently didn't want to do].

Upvotes: 2

Cyanfish
Cyanfish

Reputation: 4153

For classes, the == operator uses reference equality. Of course, structs are value types, so they can't be compared by reference. There is no default implementation of == for structs because memberwise comparison isn't always a valid comparison, depending on the type.

You can instead use the Object.Equals method, which does compare memberwise:

Console.WriteLine(user.Equals(default(User)) ? "not found" : "found");

Or you could just implement == to call Object.Equals:

public static bool operator ==(User lhs, User rhs)
{
    return lhs.Equals(rhs);
}

However, the default implementation of Equals for structs uses reflection, and so is very slow. It would be better to implement Equals yourself, along with == and != (and possibly GetHashCode too):

public override bool Equals(Object obj)
{
    return obj is User && Equals((User)obj);
}

public bool Equals(User other)
{
    return UserGuid == other.UserGuid && Username == other.Username;
}

public static bool operator ==(User lhs, User rhs)
{
    return lhs.Equals(rhs);
}

public static bool operator !=(User lhs, User rhs)
{
    return !lhs.Equals(rhs);
}

Upvotes: 40

dcastro
dcastro

Reputation: 68730

When you compare two reference types, you're checking whether the references point to the same type.

But if you're dealing with value types, there are no references to compare.

You have to implement the operator yourself, and (probably) check if the value type's fields match.

Upvotes: 0

D Stanley
D Stanley

Reputation: 152626

You just have to implement it:

public static bool operator == (User u1, User u2) 
{
   return u1.Equals(u2);  // use ValueType.Equals() which compares field-by-field.
}

Upvotes: 3

David Pilkington
David Pilkington

Reputation: 13628

You can overload the == operator if you want to do this

public static bool operator ==(User u1, User u2) 
   {
        return u1.Equals(u2)
   }

You should also override the Equals and GetHashCode()

Also if you override the ==, you will probably want to override != as well.

public static bool operator !=(User u1, User u2) 
   {
        return !u1.Equals(u2)
   }

Upvotes: 0

Related Questions