Casting from interface to underlying type
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开发者_如何学Go 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>();
}
}
}
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();
}
}
An interface is a constraint on the implementation of a minimum set of things it must do (even if "doing" is no more than throwing a NotSupportedException
; or even a NotImplementedException
). It is not a constraint that either prevents the implementation from doing more, or on the calling code.
One thing I've learned working with .NET (and with some people who are quick to jump to a hack solution) is that if nothing else, reflection will often allow people to by pass your "protections."
Interfaces are not iron shackles of programming, they're a promise that your code makes to any other code saying "I can definitely do these things." If you "cheat" and cast the interface object into some other object because you, the programmer, know something that the program doesn't, then you're breaking that contract. The consequence is poorer maintainability and a reliance that no one ever mess up anything in that chain of execution, lest some other object get sent down that doesn't cast correctly.
Other tricks like making things readonly or hiding the actual list behind a wrapper are only stop-gaps. You could easily dig into the type using reflection to pull out the private list if you really wanted it. And I think there are attributes you can apply to types to prevent people from reflecting into them.
Likewise, readonly lists aren't really. I could probably figure out a way to modify the list itself. And I can almost certainly modify the items on the list. So a readonly isn't enough, nor is a copy or an array. You need a deep copy (clone) of the original list in order to actually protect the data, to some degree.
But the real question is, why are you fighting so hard against the contract that you wrote. Sometimes reflection hacking is a handy workaround when someone else's library is poorly designed and didn't expose something that it needs to (or a bug requires that you go digging to fix it.) But when you have control over the interface AND the consumer of the interface, there's no excuse to not make the publicly exposed interface as robust as you need it to be to get your work done.
Or in short: If you need a list, don't return IEnumerable, return a List. If you've got an IEnumerable but you actually needed a list, then its safer to make a new list from that IEnum and use that. There are very few reasons (and even fewer, maybe no, good reasons) to cast up to a type simply because "I know it's actually a list, so this will work."
Yeah, you can take steps to try and prevent people from doing that, but 1) the harder you fight people who insist on breaking the system, the harder they will try to break it and 2) they're only looking for more rope, and eventually they'll get enough to hang themselves.
精彩评论