Reviewing an earlier question on SO, I started thinking about the situation where a class exposes a value, such as a collection, as an interface implemented by the value's type. In the code example below, I am using a List, and exposing that list as IEnumerable.
Exposing the list through the IEnumerable interface defines the intent that the list only be enumerated over, not modified. However, since the instance can be re-cast back to a list, the list itself can of course be modified.
I also include in the sample a version of the method that prevents modification by copying the list item references to a new list each time the method is called, thereby preventing changes to the underlying list.
So my question is, should all code exposing a concrete type as an implemented interface do so by means of a copy operation? Would there be value in a language construct that explicitly indicates "I want to expose this value through an interface, and calling code should only be able to use this value through the interface"? What techniques do others use to prevent unintended side-effects like these when exposing concrete values through their interfaces.
Please note, I understand that the behavior illustrated is expected behavior. I am not claiming this behavior is wrong, just that it does allow use of functionality other than the expressed intent. Perhaps I am assigning too much significance to the interface - thinking of it as a functionality constraint. Thoughts?
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace TypeCastTest
{
class Program
{
static void Main(string[] args)
{
// Demonstrate casting situation
Automobile castAuto = new Automobile();
List<string> doorNamesCast = (List<string>)castAuto.GetDoorNamesUsingCast();
doorNamesCast.Add("Spare Tire");
// Would prefer this prints 4 names,
// actually prints 5 because IEnumerable<string>
// was cast back to List<string>, exposing the
// Add method of the underlying List object
// Since the list was cast to IEnumerable before being
// returned, the expressed intent is that calling code
// should only be able to enumerate over the collection,
// not modify it.
foreach (string doorName in castAuto.GetDoorNamesUsingCast())
{
Console.WriteLine(doorName);
}
Console.WriteLine();
// --------------------------------------
// Demonstrate casting defense
Automobile copyAuto = new Automobile();
List<string> doorNamesCopy = (List<string>)copyAuto.GetDoorNamesUsingCopy();
doorNamesCopy.Add("Spare Tire");
// This returns only 4 names,
// because the IEnumerable<string> that is
// returned is from a copied List<string>, so
// calling the Add method of the List object does
// not modify the underlying collection
foreach (string doorName in copyAuto.GetDoorNamesUsingCopy())
{
Console.WriteLine(doorName);
}
Console.ReadLine();
}
}
public class Automobile
{
private List<string> doors = new List<string>();
public Automobile()
{
doors.Add("Driver Front");
doors.Add("Passenger Front");
doors.Add("Driver Rear");
doors.Add("Passenger Rear");
}
public IEnumerable<string> GetDoorNamesUsingCopy()
{
return new List<string>(doors).AsEnumerable<string>();
}
public IEnumerable<string> GetDoorNamesUsingCast()
{
return doors.AsEnumerable<string>();
}
}
}
Yes, you can. If you implement an interface and provide body to its methods from a class. You can hold object of the that class using the reference variable of the interface i.e. cast an object reference to an interface reference.
You can access the members of an interface through an object of any class that implements the interface. For example, because Document implements IStorable , you can access the IStorable methods and property through any Document instance: Document doc = new Document("Test Document"); doc.
If you have a concrete class, you can cast it to the interface. If you have an interface, it is possible to cast to the concrete class.
One way you can prevent this is by using AsReadOnly() to prevent any such nefariousness. I think the real answer is though, you should never be relying on anything other than the exposed interface/contract in terms of the return types, etc. Doing anything else defies encapsulation, prevents you from swapping out your implementations for others that don't use a List but instead just a T[], etc, etc.
Edit:
And down-casting like you mention is basically a violation of the Liskov Substition Principle, to get all technical and stuff.
In a situation like this, you could define your own collection class which implements IEnumerable<T>
. Internally, your collection could keep a List<T>
and then you could just return the enumerator of the underlying list:
public class MyList : IEnumerable<string>
{
private List<string> internalList;
// ...
IEnumerator<string> IEnumerable<string>.GetEnumerator()
{
return this.internalList.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.internalList.GetEnumerator();
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With