Virtual Methods
Author |
Message |
wtd
|
Posted: Sun Dec 17, 2006 4:52 pm Post subject: Virtual Methods |
|
|
Virtual Methods? What the ...?!
Well, this tutorial aims to answer exactly that question. First, though, we must cover a bit of type theory as it applies to object-oriented programming.
If you're reading this, then I'm assuming you have at least some understanding of the is-a relationship between classes. This relationship can be created through implementation or interface inheritance.
Java:
code: | class Foo { }
interface Bar { }
class Baz extends Foo implements Bar { } |
Now, the class Baz is a Foo object, and it is a Bar object.
So we can do things like:
Java:
code: | Foo a = new Foo();
Foo b = new Baz();
Baz c = new Baz();
Bar d = new Baz(); |
These all work because of the is-a relationship.
Let's add some methods.
Java:
code: | class Foo {
public String getName() {
return "Foo";
}
}
class Bar extends Foo {
public String getName() {
return "Bar";
}
} |
And now we can do the following.
Java:
code: | Foo a = new Bar();
System.out.println("a's name is " + a.getName()); |
This works because getName is a method in the Foo class. So the compiler sees no reason to complain.
What's interesting is the actual output.
Didn't we tell the compiler that the variable "a" is a Foo object? And doesn't Foo's getName method return "Foo"?
Yes, we did, and yes it does. But the actual method lookup occurs at runtime. As a result, the getName method specified in the Bar class is called.
Let's talk through this a bit more
When I said the "a" variable is of type Foo, I specified that it was allowed to be assigned any value belonging to that class. Bar objects belong to the class Foo, since they must implement at the very least the functionality present in the Foo class specification.
But the behavior of Java (and numerous other object-oriented programming languages) is not to view this as a directive to strictly interpret "a" as a Foo object.
Since this is the only behavior in so many languages, it's easy to take it for granted. There just isn't that much thought about it.
Well, not that much thought about it until one hits a language where that's not the case, and some of those languages sit at the top of their game in terms of people using them: C++, C#, and Object Pascal (Delphi).
By default, they do just the opposite of what we achieved earlier.
Some more code
C++:
code: | class Foo
{
public:
std::string get_name() const
{
return "Foo";
}
};
class Bar : public Foo
{
public:
std::string get_name() const
{
return "Bar";
}
}; |
code: | Foo *a = new Bar;
std::cout << "a's name is " << a->get_name()
<< std::endl; |
Object Pascal:
code: | program NonVirtualFunctions;
type
TFoo = class(TObject)
function Name : string;
end;
TBar = class(TFoo)
function Name : string;
end;
function TFoo.Name : string;
begin
Name := 'Foo';
end;
function TBar.Name : string;
begin
Name := 'Bar';
end;
var
A : TFoo;
begin
A := TBar.Create;
writeln(A.Name);
end. |
C#:
code: | class Foo
{
public string GetName()
{
return "Foo";
}
}
class Bar : Foo
{
public new string GetName()
{
return "Bar";
}
} |
code: | Foo a = new Bar();
Console.WriteLine("a's name is {0}", a.GetName()); |
The output:
So what's going on?
In these languages, essentially what is happening is that we're not just saying "a" is a variable containing some type in the Foo class, but that it is an object of the specific Foo class itself.
Ada95, interestingly enough, actually separates the types. There would be the Foo type, and then the Foo class, which could also contain the Bar type.
But this isn't very useful. When I create a function that can take a Foo object as an argument, I want to be able to use that function with my Bar class, and hang onto the specializations I made in the Bar class.
Imagine you have a shape class. Shapes can be passed to a draw function which draws on the shape's own "representation" method. If we use the default behavior in these languages, ever shape, no matter whether it's a circle or a square, will always be drawn the same, because the shape class' representation method is always called.
Virtual methods: the solution
Fortunately, there's a way to get the behavior we want, and that solution is declaring the methods/fucntions as virtual. Now, as in Java (and others) the method lookup is done at runtime.
C++:
code: | class Foo
{
public:
virtual std::string get_name() const
{
return "Foo";
}
};
class Bar : public Foo
{
public:
virtual std::string get_name() const
{
return "Bar";
}
}; |
code: | Foo *a = new Bar;
std::cout << "a's name is " << a->get_name()
<< std::endl; |
Object Pascal:
code: | program VirtualFunctions;
type
TFoo = class(TObject)
function Name : string; virtual;
end;
TBar = class(TFoo)
function Name : string; override;
end;
function TFoo.Name : string;
begin
Name := 'Foo';
end;
function TBar.Name : string;
begin
Name := 'Bar';
end;
var
A : TFoo;
begin
A := TBar.Create;
writeln(A.Name);
end. |
C#:
code: | class Foo
{
public virtual string GetName()
{
return "Foo";
}
}
class Bar : Foo
{
public override string GetName()
{
return "Bar";
}
} |
code: | Foo a = new Bar();
Console.WriteLine("a's name is {0}", a.GetName()); |
The output:
And now it all does what we want.
Of course, you're probably thinking, and rightly so, that the frequency with which this option is chosen is probably what convinced other languages to make it the default behavior. Polymorphism and a lot of that other good stuff just doesn't work very well without virtual methods.
Hopefully you now understand a little more of what virtual methods are, and wjhy it's important to uinderstand them. |
|
|
|
|
![](images/spacer.gif) |
Sponsor Sponsor
![Sponsor Sponsor](templates/subSilver/images/ranks/stars_rank5.gif)
|
|
![](images/spacer.gif) |
Clayton
![](http://compsci.ca/v3/uploads/user_avatars/1718239683472e5c8d7e617.jpg)
|
Posted: Sun Dec 17, 2006 6:01 pm Post subject: (No subject) |
|
|
very enlightening wtd, I understand more what virtual methods are now, at first when I read through the C++, C# etc. examples, I was a bit confused, perhaps showing what the output is would be a good idea.
EDIT: I don't think you meant to have this in the C++ example without virtual methods:
|
|
|
|
|
![](images/spacer.gif) |
rdrake
![](http://compsci.ca/v3/uploads/user_avatars/113417932472fc6c9cd916.png)
|
Posted: Sun Dec 17, 2006 7:20 pm Post subject: (No subject) |
|
|
The following code:
code: | using System;
public class Test
{
public static void Main()
{
Range r = new ExclusiveRange(1, 5);
r.Do(Console.WriteLine);
int sum = r.Reduce(delegate(int a, int b) { return a + b; }, 0);
Console.WriteLine("Sum: {0}", sum);
}
}
class Range
{
public delegate void Action(int i);
public delegate T ReductionArg<T>(T a, int b);
private int start, end;
public Range(int start, int end)
{
this.start = start;
this.end = end;
}
public virtual int Start
{
get { return start; }
}
public virtual int End
{
get { return end; }
}
public virtual int EffectiveEnd
{
get { return end; }
}
public virtual void Do(Action action)
{
for (int i = Start; i <= EffectiveEnd; i++)
{
action(i);
}
}
public virtual T Reduce<T>(ReductionArg<T> f, T init)
{
for (int i = Start; i <= EffectiveEnd; i++)
{
init = f(init, i);
}
return init;
}
}
class ExclusiveRange : Range
{
public ExclusiveRange(int start, int end) : base(start, end) { }
public override int EffectiveEnd
{
get { return End - 1; }
}
} | Will output:Quote: 1
2
3
4
Sum: 10 However, if we make one change. code: | using System;
public class Test
{
public static void Main()
{
Range r = new ExclusiveRange(1, 5);
r.Do(Console.WriteLine);
int sum = r.Reduce(delegate(int a, int b) { return a + b; }, 0);
Console.WriteLine("Sum: {0}", sum);
}
}
class Range
{
public delegate void Action(int i);
public delegate T ReductionArg<T>(T a, int b);
private int start, end;
public Range(int start, int end)
{
this.start = start;
this.end = end;
}
public virtual int Start
{
get { return start; }
}
public virtual int End
{
get { return end; }
}
public int EffectiveEnd
{
get { return end; }
}
public virtual void Do(Action action)
{
for (int i = Start; i <= EffectiveEnd; i++)
{
action(i);
}
}
public virtual T Reduce<T>(ReductionArg<T> f, T init)
{
for (int i = Start; i <= EffectiveEnd; i++)
{
init = f(init, i);
}
return init;
}
}
class ExclusiveRange : Range
{
public ExclusiveRange(int start, int end) : base(start, end) { }
public new int EffectiveEnd
{
get { return End - 1; }
}
} | We get the following output:Quote: 1
2
3
4
5
Sum: 15 But it's the same code in the method, right?
For 5 bits, somebody fully explain (using the information wtd provided above) why this occurs.
Note: Code shamelessly stolen from wtd from our discussion in the IRC channel yesterday. |
|
|
|
|
![](images/spacer.gif) |
OneOffDriveByPoster
|
Posted: Tue Dec 19, 2006 12:08 am Post subject: (No subject) |
|
|
A diff would have helped, but it was fairly obvious where the change might be.
rdrake wrote: For 5 bits, somebody fully explain (using the information wtd provided above) why this occurs.
I don't know what merits designation as a "full" explanation and I don't really do C-sharp, but here goes:
Two differences really. Using the "override" versus the "new" modifier in the sub-class, and using "virtual" or not in the base class.
The version with "override" has it such that the version of the EffectiveEnd property in the sub-class is used when the property is accessed even when a expression of base-class compile-time type (including "implicit 'this'") is used. The base-class has the method declared as "virtual", so run-time lookup is done (and finds an override for the method). "virtual" is required for "override" to be allowed.
The version with "new" has it such that the sub-class version of EffectiveEnd is separate from the base-class version of EffectiveEnd. If you access EffectiveEnd from the sub-class, you get the sub-class version, and if you access it from the base class (again, including "this"), you get the base-class version. I think it is possible to use "new" with "virtual" anyway though. That is, in the sub-class, you can shadow the EffectiveEnd of the base class.
Because of the way "virtual" works in C-sharp (I think), the lack of "virtual" in the base class will keep a version that does not make a "new" sub-class member from compiling (if you insist on having the same name and signature). |
|
|
|
|
![](images/spacer.gif) |
OneOffDriveByPoster
|
Posted: Tue Dec 19, 2006 12:11 am Post subject: (No subject) |
|
|
OneOffDriveByPoster wrote: I think it is possible to use "new" with "virtual" anyway though.
It might be more interesting if there was another sub-class. :) |
|
|
|
|
![](images/spacer.gif) |
wtd
|
Posted: Tue Dec 19, 2006 1:59 pm Post subject: (No subject) |
|
|
Perhaps. I just wasn't sure where else to take that particular example.
And I would imagine virtual could be used in the subclass, and would designate that lookups for the property EffectiveEnd when the variable is typed as an ExclusiveRange object would be done at runtime.
It'll be interesting to see how Microsoft implements type-inferencing in C# 3.0 (separate from .NET 3.0) and what effect that has.
For instance, will the compiler be smart enough to say, "if everything in the base class is virtual, and the subclass does not add any new methods, then internally type this variable as the base class, rather than the subclass"?
Of course, that could cause problems when combined with extension methods, as one of those could be defined to work only with objects belonging to the subclass. Of course, assuming all virtual methods, and no additional functionality in the subclass vs the base class, such a design would be atrocious. |
|
|
|
|
![](images/spacer.gif) |
|
|