Assumptions are the mother of all (part 2).
In my own time, I've been learning Objective-C for fun. I'm not sure why, perhaps I didn't feel I spent enough time in front of a computer. But I digress.
One of the interesting things I've learned is that the following code works.
Note that I assume we have a class named ArbitraryObject, and that it has a method that takes no parameters entitled foo.
Objective-C
ArbitraryObject * obj;
obj = nil;
[obj foo];
Interesting, this code doesn't crash. In comparison, the following languages all crash when you try to do the equivalent.
C#
ArbitraryObject obj = null;
obj.foo();
This is the easy one. We get a NullReferenceException thrown.
Java
ArbitraryObject obj = null;
obj.foo();
This is another easy one. We get a NullPointerException thrown.
Python
obj = None
obj.foo()
Since we don't have to type what obj is, we get an AttributeError talking about how the NoneType object doesn't have an attribute of foo. Not exactly the same as before, but close enough.
C++
ArbitraryObject * obj = 0;
obj->foo();
Note that this isn't the entire code required to get the program to crash (and I'm not sure what spurred me to prove to myself that it wouldn't work). The MS VC++ compiler was optimizing my defects away in different ways. I had to add an instance variable to the ArbitraryObject class, assign it in the constructor, and have foo return the variable. Then in the calling code I printf'd it. But I'm ignoring this for simplicity.
But regardless, it's an access violation error.
What we've proved is that Objective-C works very differently from how other programming languages work. There are some pros and cons of this, depending on your perspective. It's just important to know how things work.
A couple notes :
- I use nil, null, 0, and None interchangeably.
- I'm assuming the compiler hasn't optimized your (possibly intentional) errors away.
- I really have nothing against any programming language or technology, so please don't assume that I'm some [arbitrary technology/programming langauge] basher or fanboy (even though Objective-C is primarily an Apple language, and Apple is a competitor to Microsoft). This is just another example of where you might think things work one way, and work a different way completely.
- I find this particular example fascinating.
Performance
Comparing Objective-C performance to C#, Java, and Python isn't really fair as they all have to be translated into machine language at runtime.
However the C++ comparison is fair. What I'm assuming is happening in Objective-C (without diving into PPC assembly) is that there's a check before each function call that says, "Hey, if this address is nil then jump over this block of code that sets up for and executes the function call." So assumedly you have a some extra instructions every time you make a function call. Not a big deal, especially these days where we have a bazillion clock cycles per second, but definitely something that you need to think about if you're trying to squeeze every cycle out of the processor.
Program Flow
This is the main root problems I would assume. I'm going to use C# to flesh out the code, but you can easily translate it to Objective-C. More on this after the code.
Let's go back to our example code and flesh it out a bit in order to prove a point.
public class ArrayWrapper
{
private static int [] array = new int[] { 1, 2, 3, 4 };
private static int index = 10;
public static SetIndex(int index)
{
ArrayWrapper.index = index;
}
public int GetIndexedValue()
{
// note that this crashes if index is the default value
return ArrayWrapper.array[ArrayWrapper.index];
}
}
public class ArbitraryObject
{
// we don't have to define an empty constructor, but will anyways!
public ArbitraryObject() { }
public void foo()
{
ArrayWrapper.SetIndex(2);
}
}
And now for the driver code function.
public void DriverFunction(ArbitraryObject obj)
{
obj.foo();
// here we have assumed that obj.foo() did it's job and set the
// index of the ArrayWrapper to a valid value
Console.WriteLine(ArrayWrapper.GetIndexedValue().ToString());
}
Ok, so this seems like a really stupid example, but it's point is to show that you're assuming that the ArbitraryObject class worked in order for the rest of the function to work.
Let's run some tests. These are based on whether or not obj.foo( ) worked. I'm not going to translate this to Objective-C, but you can easily see what happens.
C# Tests
Valid instance of Arbitrary Object : Wrote out "3" to the console.
Null reference : NullReference exception at line xxxx.
Objective-C Tests
Valid instance of Arbitrary Object : Wrote out "3" to the console.
Null reference : Out of bounds error.
Ok, so we still get an error in Objective-C. And it's relatively easy to debug what's happening. But if the user gets this, the app will have crashed. And they'll get upset. Arguably, they'd get upset in C# too.
But what if the default index was valid? Like 0? We wouldn't crash, but we'd get incorrect results. Which is a LOT harder to debug. But doesn't crash for the user. Maybe it affects the final results that they're looking it for. Maybe it doesn't.
My point to all this is that this introduces a lot of subtleties to debugging something that you have to think about when writing or debugging the code. I'm not saying that it's bad at all. Sometimes you don't care about if the line fails or not -- and you don't have to write a bunch of function calls prefixed with if (reference != null).
Another reason to not make assumptions about the way things work (especially in new programming languages). :)