Ok, but when would you ever want that behaviour? An issue at work recently provided an example: when you can't trust that the thing your object was passed won't be changed. Or that the object you passed in turns out to be the same one passed out later. Here's an example:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
namespace StructVsClass | |
{ | |
public class PersonClassFromDb | |
{ | |
public string Name { get; set; } | |
} | |
public class PersonClassFromService | |
{ | |
public string Salutation { get; set; } | |
public int Age { get; set; } | |
} | |
public class PersonUsingClasses | |
{ | |
private readonly PersonClassFromDb fromDb; | |
private readonly PersonClassFromService fromService; | |
public string DislayName { get { return String.Format("{0} {1}", this.fromService.Salutation, this.fromDb.Name); } } | |
public PersonUsingClasses(PersonClassFromDb fromDb, PersonClassFromService fromService) | |
{ | |
this.fromDb = fromDb; | |
this.fromService = fromService; | |
// Forgot to remove this line from debugging session | |
this.fromService.Age = 0; | |
} | |
public PersonClassFromService ToServicePerson() | |
{ | |
return this.fromService; | |
} | |
} | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
Console.WriteLine("With classes:"); | |
PersonClassFromDb fromDb = new PersonClassFromDb { Name = "Frank Abagnail" }; | |
PersonClassFromService fromService = new PersonClassFromService { Salutation = "Mr.", Age = 55 }; | |
PersonUsingClasses person = new PersonUsingClasses(fromDb, fromService); | |
Console.WriteLine("Hello, " + person.DislayName); | |
Console.WriteLine("Frank changes his data outside our person class instance."); | |
fromService.Salutation = "Dr."; | |
Console.WriteLine("Hello, " + person.DislayName); | |
// Check that the class didn't change anything | |
PersonClassFromService actualServicePerson = person.ToServicePerson(); | |
if (actualServicePerson.Age != fromService.Age) | |
{ | |
Console.WriteLine("The person object changed something!"); | |
} | |
else | |
{ | |
Console.WriteLine("The person object hasn't changed anything."); | |
} | |
} | |
} | |
} | |
Output: | |
With classes: | |
Hello, Mr. Frank Abagnail | |
Frank changes his data outside our person class instance. | |
Hello, Dr. Frank Abagnail | |
The person object hasn't changed anything. |
There is a class, PersonUsingClasses, that takes items from two data sources and holds on to the references, rather than making copies of all of the properties of each. It then uses those references to construct a new property, DisplayName. I've written such code myself, unaware of the dangers. This class also has a way to convert it back to one of the data source types, ToServicePerson.
A couple of things to notice:
- The PersonClassFromService reference sent into the constructor is the same one passed out in ToServicePerson
- Someone forgot to remove his debugging code in the constructor.
One option to fix this is to not hold on to the references passed into the constructor and make copies of every property that matters to PersonClassUsingClasses. That might be a good option, but we need to support recreating the PersonClassFromService object, which could have properties that don't matter. Or, there could be dozens and dozens of properties to track. (I worked at a bank - trust me when I say this happens.)
A nearly equivalent approach is to use return structs from the DB and service:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
namespace StructVsClass | |
{ | |
public struct PersonStructFromDb | |
{ | |
public string Name { get; set; } | |
} | |
public struct PersonStructFromService | |
{ | |
public string Salutation { get; set; } | |
public int Age { get; set; } | |
} | |
public class PersonUsingStructs | |
{ | |
private readonly PersonStructFromDb fromDb; | |
private readonly PersonStructFromService fromService; | |
public string DislayName { get { return String.Format("{0} {1}", this.fromService.Salutation, this.fromDb.Name); } } | |
public PersonUsingStructs(PersonStructFromDb fromDb, PersonStructFromService fromService) | |
{ | |
this.fromDb = fromDb; | |
this.fromService = fromService; | |
// Forgot to remove this line from debugging session | |
this.fromService.Age = 0; | |
} | |
public PersonStructFromService ToServicePerson() | |
{ | |
return this.fromService; | |
} | |
} | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
Console.WriteLine("With structs:"); | |
PersonStructFromDb fromDbS = new PersonStructFromDb { Name = "Frank Abagnail" }; | |
PersonStructFromService fromServiceS = new PersonStructFromService { Salutation = "Mr.", Age = 55 }; | |
PersonUsingStructs personS = new PersonUsingStructs(fromDbS, fromServiceS); | |
Console.WriteLine("Hello, " + personS.DislayName); | |
Console.WriteLine("Frank changes his data outside our person class instance."); | |
fromServiceS.Salutation = "Dr."; | |
Console.WriteLine("Hello, " + personS.DislayName); | |
// Check that the class didn't change anything | |
PersonStructFromService actualServicePersonS = personS.ToServicePerson(); | |
if (actualServicePersonS.Age != fromServiceS.Age) | |
{ | |
Console.WriteLine("The person struct changed something!"); | |
} | |
else | |
{ | |
Console.WriteLine("The person struct hasn't changed anything."); | |
} | |
} | |
} | |
} | |
Output: | |
With structs: | |
Hello, Mr. Frank Abagnail | |
Frank changes his data outside our person class instance. | |
Hello, Mr. Frank Abagnail | |
The person struct changed something! |
Now the system copies every property in every input parameter for us because they are structs. It copies every property again when we call ToServicePerson. This means that changing the input parameter does not get reflected in PersonUsingStructs and we can detect the accidental change in the constructor. This approach uses more memory because of all of this copying, but it's not much more than if we'd made copies of every property ourselves in PersonUsingClasses. We've protected the system from accidental changes.
No comments:
Post a Comment