Click here to Skip to main content
15,885,546 members
Articles / Programming Languages / C# 5.0
Tip/Trick

Gotcha When C# Structures Implement Interfaces

Rate me:
Please Sign up or sign in to vote.
4.88/5 (8 votes)
30 Aug 2015CPOL2 min read 21.9K   4   3
Gotcha when passing in structures to a method that expects an Interface as a parameter

Introduction

This tip highlights something to be aware of when using structures that implement Interfaces in C#.

Background

Often in C#, it is useful to use structures in place of classes. Since being value types and being allocated on the stack instead of the managed heap, they are faster to create, faster to access their data (since they do not follow a pointer to the managed heap unlike reference types) and they are faster to get cleaned up. Cleaning them up on the stack only involves reallocation of a memory address on the stack at the end of the scope of their use as opposed to a reference type which must be cleaned up by the garbage collection process.

They can be used in places where using inheritance is not necessary so they can be used as DTOs for example.

Structures That Implement Interfaces

Things get interesting when using structures that implement interfaces and then passing those structures as parameters into methods that accept Interface parameters. Take a look at the code below, rather copy it and run it in a console application or a Linq Pad session.

C#
// Put this code in a Linq Pad session to run it

void Main()
{
    // Declare a struct that implements the interface IWorkItem
    var wt = new WorkItem("asdf", 5);
    
    // Try to change the structure without being cast to an interface type    
    ChangeWork(wt, "pqrs", -4);
    
    // Check to see if the values in the struct changed
    Console.WriteLine(string.Format
    ("wt - WorkType: {0} WorkHours: {1}", wt.WorkType, wt.WorkHours));
    // Humm... not changed
    
    // Ok cast it as an Interface type i.e reference type, i.e. box the value
    var iwt = wt as IWorkItem;
    
    // Structure cast to interface type, 
    // now the value is copied and boxed, so try and change it    
    ChangeWork(iwt, "pqrs", -4);
    
    // Result of the operation on the boxed value
    Console.WriteLine(string.Format
    ("iwt - WorkType: {0} WorkHours: {1}", iwt.WorkType, iwt.WorkHours));    
        
    // Original struct, wt, remains the same since only its boxed copy was changed
    Console.WriteLine(string.Format
    ("wt - WorkType: {0} WorkHours: {1}", wt.WorkType, wt.WorkHours));
    
    // Create an instance of a class that implements IWorkItem
    var wic = new WorkItemClass("hklm", 6);
    
    // Create a collection of IWorkItem types
    var lstStruct = new List<IWorkItem> { wt, wic };
    
    // Change the collection of IWorkItem types
    ChangeWorkList(lstStruct, "qwerty", -1);
    
    // Items in the collection have changed
    foreach(var i in lstStruct)
    {
       Console.WriteLine(string.Format
       ("WorkType: {0} WorkHours: {1}", i.WorkType, i.WorkHours));
    }
    
    // The value of the reference type added to the collection has changed
    // But the value of the struct outside the collection remains the same, 
    // again, it was copied and boxed
    // and only its boxed copy was changed
    Console.WriteLine(string.Format
    ("wt - WorkType: {0} WorkHours: {1}", wt.WorkType, wt.WorkHours));
    Console.WriteLine(string.Format
    ("wic - WorkType: {0} WorkHours: {1}", wic.WorkType, wic.WorkHours));
}

// Define other methods and classes here

public void ChangeWork(IWorkItem wrk, string workType, int workHours)
{
   wrk.WorkType = workType;
   wrk.WorkHours = workHours;
}

public void ChangeWorkList
(IEnumerable<IWorkItem> workList, string workType, int workHours)
{
  foreach(var i in workList)
  {
    i.WorkType = workType;
    i.WorkHours = workHours;
  }
}

public interface IWorkItem
{
   string WorkType {get; set;}
   
   int WorkHours {get; set;}
}

public class WorkItemClass : IWorkItem
{
   public int WorkHours {get; set;}
   
   public string WorkType {get; set;}   
   
   public WorkItemClass(string workType, int hours)
   {     
     WorkHours = hours;
     WorkType = workType;    
   }
}

public struct WorkItem : IWorkItem
{
   public int WorkHours {get; set;}
   
   public string WorkType {get; set;}   
   
   public WorkItem(string workType, int hours):this()
   {     
     WorkHours = hours;
     WorkType = workType;    
   }
}

What is happening in the above code is basically the boxing of a value type which is a copy of the struct being made and wrapped in an object instance a reference type at the point of entry into the method.

This is what happens implicitly when the struct is being passed into the 'ChangeWork' method the first time. The implictly created reference type is changed within the method and then goes out of scope once the method ends. The struct 'wt' meanwhile, outside the method remains unchanged.

Implicit boxing happens again when the struct 'wt' is added to a list of IWorkItems along with a reference type and changed. Again, the struct outside the method call remains unchanged.

This is something to be aware of when passing in structs that implement an interface/s to methods that expect parameters of that interface.

The struct can be changed by passing it into a method by reference but the signature of the method will have to be one that accepts the structure type rather than the interface type.

When using methods that alter the interface type, when using structures that implement said interface/s, it is important to continue processing with the interface type, such as the collection 'lstStruct' in the example, in order to harvest the changes to items in the collection as expected, instead of reverting to the element/s that formed its parts.

History

  • Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Questionmy experience with that Pin
Mr.PoorEnglish6-Sep-15 2:26
Mr.PoorEnglish6-Sep-15 2:26 
QuestionPass as ref? Pin
Paramecium133-Sep-15 8:14
professionalParamecium133-Sep-15 8:14 
AnswerRe: Pass as ref? Pin
Mr.PoorEnglish6-Sep-15 2:22
Mr.PoorEnglish6-Sep-15 2:22 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.