Reputation: 160
Take this simple java class as an example:
class SomeWrapper {
List<SomeType> internalDataStructure = new ArrayList<>();
boolean hasSomeSpecificSuperImportantElement = false;
// additional fields that do useful things
public void add(SomeType element) {
internalDataStructure.add(element);
if (SUPER_IMPORTANT.equals(element)) {
hasSomeSpecificSuperImportantElement = true;
}
}
public Iterable<SomeType> getContents() {
return internalDataStructure;
}
// additional methods that are definitely useful
}
Although it looks somewhat okay it has a big flaw. Because internalDataStructure is returned as is, callers of getContents() could class cast it to List and modify the internal data structure in ways they are not supposed to.
class StupidUserCode {
void doSomethingStupid(SomeWrapper arg) {
Iterable<SomeType> contents = arg.getContents();
// this is perfectly fine
List<SomeType> asList = (List<SomeType>) contents;
asList.add(SUPER_IMPORTANT);
}
}
In java and in other, similar programming languages, this problem can be solved by wrapping the internal data structure in some immutable wrapper. For example:
public Iterable<SomeType> getContents() {
return Collections.unmodifiableList(internalDataStructure);
}
This works now, but has, in my humble opinion, a number of drawbacks:
Are there any programming languages where you can specify a return type of a method to be impossible to be class cast?
I was thinking of something like synthetic types where appending an exclamation mark ! to the end of a typename makes it un-class-cast-able. Like this:
public Iterable!<SomeType> getContents() {
return internalDataStructure;
}
void doSomethingStupid(SomeWrapper arg) {
// The ! here is necessary because Iterable is a different type than Iterable!
Iterable!<SomeType> contents = arg.getContents();
// this now becomes a compile-time error because Iterable! can not be cast to anything
List<SomeType> asList = (List<SomeType>) contents;
asList.add(SUPER_IMPORTANT);
}
Upvotes: 2
Views: 54
Reputation: 51034
Since the point here is about mutability, the Java example of List
vs. Iterable
isn't a great example of mutable vs. immutable data types (the Iterator.remove
method mutates the underlying collection, so the List
could be corrupted by the external caller even without casting).
Let's instead imagine two types, MutableList
and ReadonlyList
, where MutableList
is the subtype, and a ReadonlyList
only prevents the user from mutating it; the list itself is not guaranteed to avoid mutation. (We cannot sensibly name the supertype ImmutableList
because no value is both a mutable and an immutable list.)
Casting from the supertype to the subtype, e.g. from ReadonlyList
to MutableList
, is called downcasting. Downcasting is unsafe, because not every value of the supertype is a value of the subtype; so either a check needs to be performed at runtime (as Java does, throwing ClassCastException
if the instance being casted does not have the right type), or the program will do something memory-unsafe (as C might do).
In theory, a language might forbid downcasting on the grounds that it is unsafe; popular programming languages don't, because it's convenient to be able to write code which you know is type-safe, but the language's type system is not powerful enough for you to write suitable type annotations which allow the type-checker to prove that the code is type-safe. And no type-checker can reasonably be expected to prove every provable property of your code. Still, in theory there is nothing stopping a language from forbidding downcasts; I just don't think many people will choose to use such a language for developing large software.
That said, I think the solution to the problem you describe would be simply not to make MutableList
a subtype of ReadonlyList
. The MutableList
class can still have a method to get a read-only view, but since there would be no subtype/supertype relation, that view would not be a value of type MutableList
so it could not be cast to the mutable type, even if you upcast to a common supertype first.
To avoid the performance cost at runtime, it could be possible for a language to have specific support for such wrappers to allow the wrapper to delegate its methods to the original list at compile-time instead of at runtime.
Upvotes: 2