Is it safe for structs to implement interfaces?

user1228 picture user1228 · Sep 15, 2008 · Viewed 60.2k times · Source

I seem to remember reading something about how it is bad for structs to implement interfaces in CLR via C#, but I can't seem to find anything about it. Is it bad? Are there unintended consequences of doing so?

public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }

Answer

ShuggyCoUk picture ShuggyCoUk · Aug 17, 2009

Since no one else explicitly provided this answer I will add the following:

Implementing an interface on a struct has no negative consequences whatsoever.

Any variable of the interface type used to hold a struct will result in a boxed value of that struct being used. If the struct is immutable (a good thing) then this is at worst a performance issue unless you are:

  • using the resulting object for locking purposes (an immensely bad idea any way)
  • using reference equality semantics and expecting it to work for two boxed values from the same struct.

Both of these would be unlikely, instead you are likely to be doing one of the following:

Generics

Perhaps many reasonable reasons for structs implementing interfaces is so that they can be used within a generic context with constraints. When used in this fashion the variable like so:

class Foo<T> : IEquatable<Foo<T>> where T : IEquatable<T>
{
    private readonly T a;

    public bool Equals(Foo<T> other)
    {
         return this.a.Equals(other.a);
    }
}
  1. Enable the use of the struct as a type parameter
    • so long as no other constraint like new() or class is used.
  2. Allow the avoidance of boxing on structs used in this way.

Then this.a is NOT an interface reference thus it does not cause a box of whatever is placed into it. Further when the c# compiler compiles the generic classes and needs to insert invocations of the instance methods defined on instances of the Type parameter T it can use the constrained opcode:

If thisType is a value type and thisType implements method then ptr is passed unmodified as the 'this' pointer to a call method instruction, for the implementation of method by thisType.

This avoids the boxing and since the value type is implementing the interface is must implement the method, thus no boxing will occur. In the above example the Equals() invocation is done with no box on this.a1.

Low friction APIs

Most structs should have primitive-like semantics where bitwise identical values are considered equal2. The runtime will supply such behaviour in the implicit Equals() but this can be slow. Also this implicit equality is not exposed as an implementation of IEquatable<T> and thus prevents structs being used easily as keys for Dictionaries unless they explicitly implement it themselves. It is therefore common for many public struct types to declare that they implement IEquatable<T> (where T is them self) to make this easier and better performing as well as consistent with the behaviour of many existing value types within the CLR BCL.

All the primitives in the BCL implement at a minimum:

  • IComparable
  • IConvertible
  • IComparable<T>
  • IEquatable<T> (And thus IEquatable)

Many also implement IFormattable, further many of the System defined value types like DateTime, TimeSpan and Guid implement many or all of these as well. If you are implementing a similarly 'widely useful' type like a complex number struct or some fixed width textual values then implementing many of these common interfaces (correctly) will make your struct more useful and usable.

Exclusions

Obviously if the interface strongly implies mutability (such as ICollection) then implementing it is a bad idea as it would mean tat you either made the struct mutable (leading to the sorts of errors described already where the modifications occur on the boxed value rather than the original) or you confuse users by ignoring the implications of the methods like Add() or throwing exceptions.

Many interfaces do NOT imply mutability (such as IFormattable) and serve as the idiomatic way to expose certain functionality in a consistent fashion. Often the user of the struct will not care about any boxing overhead for such behaviour.

Summary

When done sensibly, on immutable value types, implementation of useful interfaces is a good idea


Notes:

1: Note that the compiler may use this when invoking virtual methods on variables which are known to be of a specific struct type but in which it is required to invoke a virtual method. For example:

List<int> l = new List<int>();
foreach(var x in l)
    ;//no-op

The enumerator returned by the List is a struct, an optimization to avoid an allocation when enumerating the list (With some interesting consequences). However the semantics of foreach specify that if the enumerator implements IDisposable then Dispose() will be called once the iteration is completed. Obviously having this occur through a boxed call would eliminate any benefit of the enumerator being a struct (in fact it would be worse). Worse, if dispose call modifies the state of the enumerator in some way then this would happen on the boxed instance and many subtle bugs might be introduced in complex cases. Therefore the IL emitted in this sort of situation is:

IL_0001:  newobj      System.Collections.Generic.List..ctor
IL_0006:  stloc.0     
IL_0007:  nop         
IL_0008:  ldloc.0     
IL_0009:  callvirt    System.Collections.Generic.List.GetEnumerator
IL_000E:  stloc.2     
IL_000F:  br.s        IL_0019
IL_0011:  ldloca.s    02 
IL_0013:  call        System.Collections.Generic.List.get_Current
IL_0018:  stloc.1     
IL_0019:  ldloca.s    02 
IL_001B:  call        System.Collections.Generic.List.MoveNext
IL_0020:  stloc.3     
IL_0021:  ldloc.3     
IL_0022:  brtrue.s    IL_0011
IL_0024:  leave.s     IL_0035
IL_0026:  ldloca.s    02 
IL_0028:  constrained. System.Collections.Generic.List.Enumerator
IL_002E:  callvirt    System.IDisposable.Dispose
IL_0033:  nop         
IL_0034:  endfinally  

Thus the implementation of IDisposable does not cause any performance issues and the (regrettable) mutable aspect of the enumerator is preserved should the Dispose method actually do anything!

2: double and float are exceptions to this rule where NaN values are not considered equal.