Unfathomable Bugs #5: Readonly or not
Normally I post on Tuesdays, but I already have the next two posts written and this issue is too short and sweet to wait two and a half weeks. I’m actually quite surprised I haven’t stumbled on it before.
Today’s bug comes courtesy of Microsoft. Thank you, Microsoft, this series wouldn’t exist without the generous support of entities like you.
—
I was writing an extension method that would expose an IList<T> as an IReadOnlyList<T> when I came across the problem.
Because of historical coincidence, IReadOnlyList<T> is not implemented by IList<T>. As a result, some classes that implement IList<T> don’t implement IReadOnlyList<T>. A somewhat related problem is that many classes that implement IReadOnlyList<T> are readable but not readonly (example error case: you pass a read only list to some dynamic binding code, which sees that it’s actually a mutable list, and end up with two-way binding instead of one-way). The extension method I was writing was for working around these issues, and looked like this:
public static IReadOnlyList<T> AsReadOnlyList(IList<T> list) {
if (list == null) throw new ArgumentNullException("list");
if (list.IsReadOnly) {
var r = list as IReadOnlyList<T>;
if (r != null) return r;
}
return new ReadOnlyCollection<T>(list);
}
Notice the special case that doesn’t wrap the input if it is already a truly readonly list. To confirm that it was working properly, I tested some cases where wrapping should and shouldn’t occur. This resulted in a very confusing failure:
var r = new int[5];
Assert.AreNotSame(r, r.AsReadOnlyList()); // fails
Arrays aren’t being wrapped! This is despite the fact that Array.IsReadOnly is documented as guaranteed to return false (even for 0-length arrays where it would be fine if it returned true). This, you might reasonably expect, would seem to guarantee that a branch conditioned on the IsReadOnly property would not be taken. I even confirmed that the documentation was correct:
var r = new int[5];
Assert.IsFalse(r.IsReadOnly); // passes
Assert.AreNotSame(r, r.AsReadOnlyList()); // still fails. what.
What the heck is going on?? It took some very confused experimenting, but the issue is that Array.IsReadOnly is always false, until you cast to an IList<T>, in which case it is always true. Despite the fact that the array-as-IList<T> still allows you to make changes:
var r = new int[5];
Assert.IsFalse(r.IsReadOnly); // passes
IList<T> c = r;
c[0] += 1; // works. clearly not readonly
Assert.IsFalse(c.IsReadOnly); // fails
How is this possible? Well, the array type has an explicit implementation of ICollection<T>.IsReadOnly, distinct from the public Array.IsReadOnly getter, that returns true instead of false. That’s right, an on-purpose differing explicit implementation of the “same” method. This bug is coming from inside the house.
But seriously, this “bug” is by design. Apparently when Microsoft decided to not keep the IsFixedSize property when making a generic variant of IList, they also decided to subtly change the meaning of IsReadOnly. However, since changing the result of the existing method would be a breaking change, they just decided to… have a little bit of both meanings. Ugh.
I’m not the first to notice this issue. I probably won’t be the last, either. Keep it in mind, because to write actually correct code you have to check for arrays in addition to checking IsReadOnly:
public static bool IsReallyReallyReadOnly<T>(this IList<T> list) {
return list.IsReadOnly && !(list is T[]);
}
—
Discuss on Reddit
—
Twisted Oak Studios offers consulting and development on high-tech interactive projects. Check out our portfolio, or Give us a shout if you have anything you think some really rad engineers should help you with.
Older Posts
- Unfathomable Bugs #6: Pretend Precision
- My Bug, My Bad #3: Accidentally Attacking WarCraft 3
- Collapsing Types vs Monads (followup)
- Collapsing Futures: Easy to Use, Hard to Represent
- Eventual Exceptions vs Programming in a Minimal Functional Style
- The Mystery of Flunf
- Explain it like I’m Five: The Socialist Millionaire Problem and Secure Multi-Party Computation
- Computer Science Blows My Mind
- A visit to Execution Labs in Montréal
- Transmuting Dice, Conserving Entropy
- Rule of Thumb: Ask for the Clock
- Rule of Thumb: Use Purposefully Weakened Methods
- Rule of thumb: Preconditions Should be Checked Explicitly
- Intersecting Linked Lists Faster
- Mouse Path Smoothing for Jack Lumber
- My Bug, My Bad #2: Sunk by Float
- Repeat Yourself Differently
- Grover’s Quantum Search Algorithm
- Followup to Non-Nullable Types vs C#
- Optimizing Just in Time with Expression Trees
- When One-Way Latency Doesn’t Matter
- Determining exactly if/when/where a moving line intersected a moving point
- Emulating Actors in C# with Async/Await
- Making an immutable queue with guaranteed constant time operations
- Improving Checked Exceptions
- Perishable Collections: The Benefits of Removal-by-Lifetime
- Decoupling shared control
- Decoupling inlined UI code
- Linq to Collections: Beyond IEnumerable<T>
- Publish your .Net library as a NuGet package
- When null is not enough: an option type for C#
- Minkowski sums: examples
- My Bug, My Bad #1: Fractal Spheres
- Working around the brittle UI Virtualization in Windows 8
- Encapsulating Angles
- Unfathomable Bugs #4: Keys that aren’t
- How would I even use a monad (in C#)?
- Useful/Interesting Methods #1: Observable.WhenEach
- Unfathomable Bugs #3: Stringing you along
- Anonymous Implementation Classes – A Design Pattern for C#
- Tasks for ActionScript 3 – Improving on Event-Driven Programming
- Minkowski sums and differences
- Non-Nullable Types vs C#: Fixing the Billion Dollar Mistake
- Unfathomable Bugs #2: Slashing Out
- Script templates and base classes
- Unity font extraction
- Abusing “Phantom Types” to Encode List Lengths Into Their Type
- Constructive Criticism of the Reactive Extensions API
- Quaternions part 3
- Quaternions part 2
- Quaternions part 1
- Unfathomable Bugs #1: You can have things! You can have things IN things! You can have …
- Coroutines – More than you want to know
- Asset Bundle Helper
- The Visual Studio goes away
- .Net’s time traveling StopWatch
- Polish
- Introducing Catalyst
