From AS3 to C#, Part 6: Extension Methods and Virtual Functions
Today’s article continues from the last two in discussing features of C# classes that AS3 doesn’t have. We’ll discuss extension methods and the virtual function system that trips up so many C# newcomers.
Table of Contents
- From AS3 to C#, Part 1: Class Basics
- From AS3 to C#, Part 2: Extending Classes and Implementing Interfaces
- From AS3 to C#, Part 3: AS3 Class Parity
- From AS3 to C#, Part 4: Abstract Classes and Functions
- From AS3 to C#, Part 5: Static Classes, Destructors, and Constructor Tricks
- From AS3 to C#, Part 6: Extension Methods and Virtual Functions
- From AS3 to C#, Part 7: Special Functions
- From AS3 to C#, Part 8: More Special Functions
- From AS3 to C#, Part 9: Even More Special Functions
- From AS3 to C#, Part 10: Alternatives to Classes
- From AS3 to C#, Part 11: Generic Classes, Interfaces, Methods, and Delegates
- From AS3 to C#, Part 12: Generics Wrapup and Annotations
- From AS3 to C#, Part 13: Where Everything Goes
- From AS3 to C#, Part 14: Built-in Types and Variables
- From AS3 to C#, Part 15: Loops, Casts, and Operators
- From AS3 to C#, Part 16: Lambdas and Delegates
- From AS3 to C#, Part 17: Conditionals, Exceptions, and Iterators
- From AS3 to C#, Part 18: Resource Allocation and Cleanup
- From AS3 to C#, Part 19: SQL-Style Queries With LINQ
- From AS3 to C#, Part 20: Preprocessor Directives
- From AS3 to C#, Part 21: Unsafe Code
- From AS3 to C#, Part 22: Multi-Threading and Miscellany
- From AS3 to C#, Part 23: Conclusion
In the last article we discussed static classes and saw this example of a pair of conversion functions between degrees and radians:
public static class MathHelpers { public const double DegreesToRadians = Math.PI / 180.0; public const double RadiansToDegrees = 180.0 / Math.PI; public static double ConvertDegreesToRadians(double degrees) { return degrees * DegreesToRadians; } public static double ConvertRadiansToDegrees(double radians) { return radians * RadiansToDegrees; } } double degrees = 180; double radians = MathHelpers.ConvertDegreesToRadians(degrees); // 3.14...
Another way this could have been implemented is to add a pair of “extension methods” to the double
type. This requires a static class with function taking a special this
parameter like so:
public static class DoubleExtensions { public const double DegreesToRadians = Math.PI / 180.0; public const double RadiansToDegrees = 180.0 / Math.PI; public static double ConvertDegreesToRadians(this double degrees) { return degrees * DegreesToRadians; } public static double ConvertRadiansToDegrees(this double radians) { return radians * RadiansToDegrees; } } double degrees = 180; double radians = degrees.ConvertDegreesToRadians(); // 3.14...
Extension methods are purely syntax sugar. When you write this:
degrees.ConvertDegreesToRadians()
The compiler just transforms it into this:
DoubleExtensions.ConvertDegreesToRadians(degrees)
That means that your extension methods aren’t really part of the class, so they can’t access private variables, functions, or properties. They also can’t add variable fields or properties, only functions. Still, some programmers will prefer the aesthetics of extension methods since they make the functions you “add on” to the class look just like the functions the class had to begin with. Others will object to them as it can be more difficult to track down where the method is implemented.
There is one “gotcha” to avoid with extension methods. They aren’t available to your class if it’s not in the same namespace as the class with the extension methods and you don’t have a using
statement for that namespace either. For example:
////////////////////// // StringExtensions.cs ////////////////////// namespace MyLib { public static class StringExtensions { public static bool IsNullOrEmpty(this String str) { return str == null || str.Length == 0; } } } ///////////////// // Application.cs ///////////////// namespace MyApp { public class Application { public Application(String name) { // Compiler error because IsNullOrEmpty is in a // namespace that isn't visible if (name.IsNullOrEmpty()) { throw new Exception("Name can't be empty"); } } } }
The Application
class would need to add a using MyLib;
statement in order to have visibility to the IsNullOrEmpty
extension method.
In AS3, there are no extension methods. Static “helper” functions are usually used instead, perhaps in a class whose constructor always throws an exception to mimic static classes at run-time:
public class NumberHelpers { public static const DEGREES_TO_RADIANS:Number = Math.PI / 180.0; public static const RADIANS_TO_DEGREES:Number = 180.0 / Math.PI; public function NumberHelpers() { throw new Error("NumberHelpers is static"); } public static function convertDegreesToRadians(degrees:Number): Number { return degrees * DEGREES_TO_RADIANS; } public static function convertRadiansToDegrees(radians:Number): Number { return radians * RADIANS_TO_DEGREES; } } var degrees:Number = 180; var radians:Number = NumberHelpers.convertDegreesToRadians(degrees); // 3.14...
Next, let’s discuss “virtual functions”. This is only a new concept to AS3 programmers because all non-final
function in AS3 are “virtual”. C# changes this by allowing “non-virtual” functions.
So what is a “virtual function”? It’s simply a function that can be overridden by a deriving class in the way that AS3 works by default. Here’s an example:
public class Shape { // Note: virtual keyword used to make this a virtual function public virtual String PrintDescription() { return "Shape"; } } public class Circle : Shape { private double radius; public Circle(double radius) { this.radius = radius; } // Note: override keyword used to override Shape's version public override String PrintDescription() { return "Circle [radius=" + radius + "]"; } } Circle circle = new Circle(10); circle.PrintDescription(); // "Circle [radius=10]" Shape shape = new Circle(10); shape.PrintDescription(); // "Circle [radius=10]"
Note that the virtual
keyword was added to the PrintDescription
function in Shape
. Without this keyword, we would have received a compiler warning on the PrintDescription
function in Circle
. This is because a non-virtual function is not expecting to be overridden. We’ll see why that is by overriding the compiler warning by adding the new
keyword to the PrintDescription
function in Circle
:
public class Shape { // Note: virtual keyword NOT used. This is a non-virtual function. public String PrintDescription() { return "Shape"; } } public class Circle : Shape { private double radius; public Circle(double radius) { this.radius = radius; } // Note: override keyword NOT used to override Shape's version // new keyword used to make our own version public new String PrintDescription() { return "Circle [radius=" + radius + "]"; } } Circle circle = new Circle(10); circle.PrintDescription(); // "Circle [radius=10]" Shape shape = new Circle(10); shape.PrintDescription(); // "Shape"
The big difference here is right at the end where we use a variable of type Shape
and call its PrintDescription
. Since the PrintDescription
function is non-virtual in Shape
, derived classes can’t override
it. The PrintDescription
in Circle
is not considered, even if the actual Shape
instance is a Circle
.
Essentially, the new
keyword has created a new version of PrintDescription
that is similar to an overloaded function. When we call Shape.PrintDescription
we are calling a different function than when we call Circle.PrintDescription
.
Note that this is the default behavior in C# and that you need to add the virtual
keyword to return to the AS3 way. Because of this, many programmers new to C# are surprised at their first compiler warning when overriding a non-virtual function. The warning is there to alert you that your overriding will not work in the way you’d expect with virtual functions and you should add the new
keyword essentially to waive this warning.
This concludes today’s topics but we’ll summarize with a comparison between C# and AS3 covering extension methods and virtual functions from this article:
//////// // C# // //////// public static class StringExtensions { public static bool IsNullOrEmpty(this String str) { return str == null || str.Length == 0; } } public class Shape { private static int NextID = 1; private int id; public Shape() { id = NextID++; } // virtual function public virtual String PrintDescription() { return "Shape"; } // non-virtual function public int GetID() { return id; } } public class Circle : Shape { private double radius; public Circle(double radius) { this.radius = radius; } // function overriding a virtual function public override String PrintDescription() { return "Circle [radius=" + radius + "]"; } // function with the same name as a base class function // but does not override the base class function public new int GetID() { return 0; } }
///////// // AS3 // ///////// public class StringExtensions { public function StringExtensions() { throw new Error("StringExtensions is static"); } public static function IsNullOrEmpty(str:String): Boolean { return str == null || str.length == 0; } } public class Shape { private static var NEXT_ID:int = 1; private var id:int; public Shape() { id = NEXT_ID++; } // virtual function public function PrintDescription(): String { return "Shape"; } // non-virtual function // MUST be final in AS3 public final function GetID(): int { return id; } } public class Circle extends Shape { private var radius:Number; public function Circle(radius:Number) { this.radius = radius; } // function overriding a virtual function public override function PrintDescription(): String { return "Circle [radius=" + radius + "]"; } // function with the same name as a base class function // but does not override the base class function // {impossible in AS3} }
Next week’s article will continue discussing the C# class system with even more features that aren’t in AS3. Stay tuned!
Spot a bug? Have a question or suggestion? Post a comment!