Working with Futures
in Dart, I've come across an interesting issue.
import 'dart:async';
class Egg {
String style;
Egg(this.style);
}
Future cookEggs(List<Egg> list) =>
new Future(() =>
['omelette','over easy'].forEach((_) => list.add(new Egg(_)))
);
Future cookOne(Egg egg) => new Future(() => egg = new Egg('scrambled'));
void main() {
List<Egg> eggList = new List();
Egg single;
cookEggs(eggList).whenComplete(() => eggList.forEach((_) => print(_.style));
cookOne(single).whenComplete(() => print(single.style));
}
The expected output is:
omelette
over easy
scrambled
The cookEggs
function that gets the List<Eggs>
works fine, but accessing the style
property of single
is unsuccessful and throws a NoSuchMethodError
.
I first thought that this might have something to do with pass-by-reference, but I don't see why Dart would pass a List
by reference but not an Egg
. Now I'm thinking that the discrepancy may have something to do with the assignment (=
) operator and the List.add()
method. I'm stuck in that train of thought, and I don't know how to test my hypothesis.
Any thoughts?
The program's output and stack trace is show below:
omelette
over easy
Uncaught Error: The null object does not have a getter 'style'.
NoSuchMethodError: method not found: 'style'
Receiver: null
Arguments: []
Stack Trace:
#0 Object.noSuchMethod (dart:core-patch/object_patch.dart:45)
#1 main.<anonymous closure> (file:///path/to/eggs.dart:20:51)
#2 _rootRun (dart:async/zone.dart:719)
#3 _RootZone.run (dart:async/zone.dart:862)
#4 _Future._propagateToListeners.handleWhenCompleteCallback (dart:async/future_impl.dart:540)
#5 _Future._propagateToListeners (dart:async/future_impl.dart:577)
#6 _Future._complete (dart:async/future_impl.dart:317)
#7 Future.Future.<anonymous closure> (dart:async/future.dart:118)
#8 _createTimer.<anonymous closure> (dart:async-patch/timer_patch.dart:11)
#9 _handleTimeout (dart:io/timer_impl.dart:292)
#10 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:124)
Unhandled exception:
The null object does not have a getter 'style'.
NoSuchMethodError: method not found: 'style'
Receiver: null
Arguments: []
#0 _rootHandleUncaughtError.<anonymous closure>.<anonymous closure> (dart:async/zone.dart:713)
#1 _asyncRunCallbackLoop (dart:async/schedule_microtask.dart:23)
#2 _asyncRunCallback (dart:async/schedule_microtask.dart:32)
#3 _asyncRunCallback (dart:async/schedule_microtask.dart:36)
#4 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:128)
Quick answer: what gets passed to your functions cookEggs
and cookOne
are references to the objects, not to the variables (which would be real pass-by-reference).
The term pass-by-reference is often misused to mean pass-references-by-value: many languages only have pass-by-value semantics, where the values that are passed around are references (i.e. pointers, without the dangerous features). See Is Java "pass-by-reference" or "pass-by-value"?
cookEggs(eggList)
……the variable eggList
contains a reference to a list of eggs. That reference was initially given to you by the expression new List()
, and you're passing it to cookEggs
after storing meanwhile in your variable eggList
. Inside cookEggs
, adding to the list works, because you passed a reference to an actual, existing list object.
cookOne(single)
……the variable single
has only been declared, so it was implicitly initialized by the language runtime to the special reference null
. Inside cookOne
, you're replacing which reference is contained in egg
; since single
is a different variable, it still contains null
, therefore the code fails when you try to use that.
The pass-references-by-value behavior is common to a lot of modern languages (Smalltalk, Java, Ruby, Python…). When you pass an object, you're actually passing-by-value (therefore copying) the contents of your variable, which is a pointer to the object. You never control where objects really exist.
Those pointers are named references rather than pointers, because they are restricted to abstract away the memory layout: you can't know the address of an object, you can't peek at the bytes around an object, you can't even be sure that an object is stored at a fixed place in memory, or that it's stored in memory at all (one could implement object references as UUIDs or keys in a persistent database, as in Gemstone).
In contrast, with pass-by-reference, you'd conceptually pass the variable itself, not its contents. To implement pass-by-reference in a pass-by-value language, you would need to reify variables as ValueHolder objects that can be passed around and whose contents can be changed.