Objective-C/Smalltalk
These are the rules for initializers in Smalltalk and Objective-C:- An "initializer" is a normal method and a normal message send.
- There is no second rule.
alloc
or new
) to the class?
No there isn't, it's just a convenient and obvious place to put it since we don't
have the instance yet and the class exists and is an obvious place to go to for
instances of that class. However, we could just
as well ask a different class to create the object for us.
The same goes with calling super
. Yes, that's usually
a good idea, because usually you want the superclass's behavior,
but if you don't want the superclass's behavior, then don't call.
Again, this is not a special rule for initializers, it usually
follows from what you want to achieve. And sometimes it doesn't, just
like with any other method you override: sometimes you call super
,
sometimes you do not.
The same goes for assigning the return value, doing the
self=[super init];
dance. Again, this is not
at all required by the language or the frameworks, although
apparently it is a common misconception that it is, a
misconception that is, IMHO, promoted by careless creation
of "best practices" as "immutable rules", something I wrote
about earlier when talking about the useless typing out of the
id
type in method declarations.
However, returning self and using the returned value is a useful
convention, because it makes it possible for init
methods to return a different object than what they started
with (for example a specific subclass or a singleton).
Swift initializers
Apple's new Swift language has taken a page from the C++ and Java playbooks and made initialization a special case. Well, lots of special cases actually. The Swift book has 30 pages on initialization, and they aren't just illustration and explanation, they are dense with rules and special cases. For example:- You can set a default value of a property in the variable definition.
- Or you can set the default value in an initializer.
- Designated initializers are now a first class language construct.
- Parameterized initializers have local and external parameter names, line methods.
- Except that the first parameter name is different and so Swift automatically provides and external parameter name for all arguments, which it doesn't with methods.
- Constant properties aren't constant in initializers.
- Swift creates a default initializer for both classes and structs.
- Swift also creates a default member wise initializer, but only for structs.
- Initializers can (only) call other initializers, but there are special rules for what is and is not allowed and these rules are different for structs and classes.
- Providing specialized initializers removes the automatically-provided default initializers.
- Initializers are different from other methods in that they are not inherited, usually.
- Except that there are specific circumstances where they are inherited.
- Confused yet? There's more!
- If your subclass provides no initializers itself, it inherits all the superclass's initializers
- If your subclass overrides all the superclass's designated initializers, it inherits all the convenience initializers (that's also a language construct). How does this not break if the superclass adds initializers? I think we've just re-invented the fragile-base-class problem.
- Oh, and you can initialize instance variables with the values returned by closures or functions.
Particularly, it is not possible to substitute a different value
or return nil
to indicate failure to initialize, nor
is it possible to call other methods (as far as I can tell).
To actually provide these useful features, we need something else:
- Use the Factory method pattern to actually do the powerful stuff you need to do ...
- ...which gets you back to where we were at the beginning with Objective-C or Smalltalk, namely sending a normal message.
So with all due respect to Michael A. Jackson:
First rule of baking programming conventions into the language: Don't do it!
The second rule of baking programming conventions into the language (experts only): Don't do it yet!
Marcel, I totally agree with your simplicity goal, but this isn't practical unless you are willing to sacrifice non-default initializable types (e.g. non-nullable pointers) or memory safety.
ReplyDeleteChris, thanks for your comment. It deserves a longer answer.
ReplyDeleteThe point that worries me the most is #15 (I think you’re right that this creates a new form of fragility). I think I’d agree with Chris that most of the others are a legitimate part of the trade-off between flexibility and the ability to provide certain language features and/or to perform certain compile-time optimisations.
ReplyDeleteChris,
ReplyDeleteThe memory safety argument is completely bogus, many languages such as Smalltalk provide perfect memory safety without special-cased initializers.
That simplicity is the part that is "impractical" is a null argument, i.e. you are simply stating that the features you prefer are non-negotiable and everything else is subordinate.
Let me turn it around: Chris, I totally agree with your goal of initializable types, but it is just not practical unless you are willing to sacrifice simplicity, parsimony and power (and ignore the fact that it doesn't actually work).
However, as I wrote, the fact is that special initializers have been tried and found to actually not work in practice, meaning users have to resort to the Factory Pattern, at which point your safety guarantees go out the window anyway. So you have arrived at the same place that you would have had you not introduced special-case initializers, except with a lot more unnecessary complexity.
Alastair, you make a good point, but Chris does not present this as a trade-off, with benefits and costs, but simply as an absolute.
You can call other functions, methods from with initialisers. You just have to do it after its variables are finished initialising and any super classes have finished initialising. At that point the object is fully initialised (memory wise) before the init function has completed.
ReplyDeleteThere are certainly a lot of rules for specific cases for initialisers, and while I love me some named parameters, it would be nice to see some more consistent behaviour across functions, methods and initialisers. But if those rules are not implements at the language rules, then they still need to be implemented at a higher level, which means the programmer still has to worry about, and probably make a few of them up themselves. That results in inconsistent behaviour across codebases which ultimately increases complexity. At leas this way, there are one set of rules for initialisers that everyone must follow. If your not willing to compromise on simplicity and power, then you should probably be writing all your code in assembly.
What is a language if not a baked list of conventions?
I came from Java and was puzzled by the bizarre complexity of Obj-C initializers. Swift is more like Java so it makes more sense to me. Yes initializers are special cases, as they should be - there are many bugs related to initialization.
ReplyDeleteThe frequency with which I've wanted initializers that can fail is less than 1 in a thousand, if I had to take a guess. In Java, I'd just throw an exception in that case. It makes no sense to do what Obj-C does which is to treat failure to initialize as a case that always need to be kept in mind.