C# and Ruby Classes






4.74/5 (16 votes)
A deep dive into the differences and similarities between C# and Ruby classes
(Apologies for the formatting of the tables - I'm working on improving the side-by-side comparisons.)
Contents
- Introduction
- Go to the head of the Class
- Derived Classes
- Constructors
- Class Access Modifiers
- Class Type Modifiers
- Static Classes: Creating a Static Class in Ruby
- Abstract Classes: Creating an Abstract Class by Raising an Initializer Exception
- Creating a Sealed Singleton Class by Using the private_class_method Function
- Several Things we Sometimes do with Classes
- Converting a Class To A String
- Equivalence and Hash Codes
- Comparability
- Dispose and Finalizers
- Property Assignment During Construction
- Interfaces
- Multiple Inheritance (mix-ins and modules)
- Fields and Properties
- Fields
- Properties
- Read-Only Properties
- Read Only Fields
- Static Properties (Class Properties)
- Computational Getters and Setters
- Events
- Methods
- Testing
- References
Introduction
Ruby is an interpreted object oriented language, as well as having characteristics similar to functional languages. It's also known as a duck-typing language. From wikipedia:
In computer programming with object-oriented programming languages, duck typing is a style of dynamic typing in which an object's methods and properties determine the valid semantics, rather than its inheritance from a particular class or implementation of a specific interface. The name of the concept refers to the duck test, attributed to James Whitcomb Riley (see history below), which may be phrased as follows:
When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.
In this article, I'm going to compare and contrast Ruby class definitions with C#. There are already numerous articles on the subject, for example Brad Cunningham's blog post Ruby for the C# developer - The Basics. What I'm endeavoring to accomplish here is a much more comprehensive discussion than one normally encounters in shorter blog entries. To accomplish this, I've drawn from a variety of sources which I provide in the References section at the end of the article. Throughout this article I also try to adhere to the correct naming conventions for C# and Ruby. In Ruby, classes (which are considered constants) begin with a capital letter. Everything else is lower case, and word groups for methods and attributes are separated by underscores, for example "MyClass" and "my_method".
Go to the head of the Class
"A class isn't just a definition, it's also a living object."5
Classes in both languages have similarities:
- They define fields (usually non-public)
- They define properties to access those fields
- They define methods that take parameters and operate on those fields
However, there is considerable divergence with regards to access modifiers such as abstract, sealed, internal, and so forth. These will be discussed first.
Defining an empty class in C# vs. Ruby:
C# | Ruby |
class CSharpClass { } |
class RubyClass end |
The most notable difference here is the use of "end" instead of curly braces.
Derived Classes
The syntax for working with base classes and derived classes is similar:
C# | Ruby |
class BaseClass { } class DerivedClass : BaseClass { } |
class BaseClass end class DerivedClass < BaseClass end |
Constructors
Let's go over constructors first, as I will be using them when I illustrate some techniques for creating abstract classes in Ruby. For the moment, let's assume public accessibility on the C# side.
C# | Ruby |
public class BaseClass { public BaseClass() { Console.WriteLine("BaseClass constructor."); } } public class DerivedClass : BaseClass { public DerivedClass() { Console.WriteLine("DerivedClass constructor."); } } class Program { static void Main(string[] args) { new DerivedClass(); } } |
class BaseClass def initialize puts "BaseClass initializer" end end class DerivedClass < BaseClass def initialize puts "DerivedClass initializer" end end DerivedClass.new |
Base Class Constructors Are Not Automatically Called in Ruby
Observe the difference in the output with C# and Ruby (running Interactive Ruby Console):
C# | Ruby |
|
|
In Ruby, the base class constructor must be called explicitly using the "super" keyword:
class DerivedClass < BaseClass def initialize super puts "DerivedClass initializer" end end
which results in the same behavior as with C#:
Class Access Modifiers
C# supports the following access modifiers on a class:
- public - the class can be accessed by any other file or assembly. If omitted, the class behaves as if "internal" were specified.
- sealed - prevents other classes from inheriting from a class with this access modifier.
- internal - restricts accessibility to the class to within the files in the same assembly.
In Ruby, there is no equivalent - public, private and protected apply only to methods1. As has been said about Ruby "In a duck-typing language, the focus is on methods, not on classes/types..."2
Class Type Modifiers
C# classes also allow two type modifiers:
- static - indicates that a class contains only static fields, properties, and/or methods.
- abstract - indicates that a class is intended only to be a base class of other classes. An abstract class cannot be instantiated directly.
In Ruby, there also are no direct equivalents to static and abstract. However, a Ruby class can be coerced into looking like a static or abstract class, which we will explore next.
Static Classes: Creating a Static Class in Ruby
In Ruby, static variables are specified using a double-ampersand (@@) operator and static methods use the class name prepended to the function name:
C# | Ruby |
public static class StaticClass { private static int foo = 1; public static int GetFoo() { return foo; } } |
class StaticClass @@foo = 1 def StaticClass.get_foo @@foo end end |
Which, if get_foo is called, results in the expected behavior:
Abstract Classes: Creating an Abstract Class by Raising an Initializer Exception
One approach is to raise an exception in the initializer (the equivalent of the constructor):
class AbstractRubyClass def initialize if self.class == AbstractRubyClass raise "Cannot instantiate an abstract class." end puts "AbstractRubyClass initializer" end end class ConcreteRubyClass < AbstractRubyClass def initialize super puts "ConcreteRubyClass initializer" end end
We can now instantiate the concrete class the usual way:
ConcreteRubyClass.new
resulting in:
However, if we try to instantiate the abstract class, we get an exception:
This magic works because of the base class verifies that the class instance is not itself -- if it is, it raises the exception. For other ways of creating abstract classes, see here. One of the things about Ruby is that there is usually multiple ways of doing the same thing, and each has their pros and cons.
Creating a Sealed Singleton Class by Using the private_class_method Function
Having covered static and abstract type modifiers, it is seems appropriate to now take a look at singleton classes. A singleton class can be created by changing the accessibility of the "new" function3, similar to how the constructor of a C# class is changed from public to protected or private:
C# | Ruby |
public class SingletonClass { private SingletonClass() { Console.WriteLine("SingletonClass constructor."); } public static SingletonClass Create() { instance = instance ?? new SingletonClass(); return instance; } } |
class SingletonClass private_class_method :new def initialize puts "SingletonClass initializer" end def SingletonClass.create(*args, &block) @@inst ||= new(*args, &block) end end |
Note that the above Ruby code is an example of a static method, which is covered at the end of this article. Resulting in an error if we try to instantiate the class, but calling the initializer function once and only once when we use the static create method:
For completeness, here's a brief explanation of some of the Ruby syntax:
- *args - a parameter beginning with an asterisk (*) indicates a variable number of arguments
- &block - this is essentially a lambda expression that can be passed in to the constructor
- @@inst is a class variable (aka a static property of the class.)
- ||= - this assigns the evaluated value on the right only when the l-value is nil
- Similar to functional languages, the last computed value is automatically returned, hence no explicit return statement is required.
In C#, one can derive from a singleton class if the constructor is protected - for this reason, a C# singleton class' constructor should actually be marked private. However, in Ruby, even though the private is actually more like C#'s protected accessibility, the above Ruby class cannot be derived. For example, this:
class ConcreteRubyClass < SingletonClass def initialize super puts "ConcreteRubyClass initializer" end end
Results in a runtime error:
Several Things we Sometimes do with Classes
Robert Klemme has a excellent blog4 on features of a class that we all take for granted, from which the following four sections are derived.
Converting a Class To A String
All C# types, being derived from Object, implement ToString(). The equivalent in Ruby is "to_s":
C# | Ruby |
public abstract class Vehicle { } public class Car : Vehicle { public override string ToString() { return "Car"; } } public class Boat : Vehicle { public override string ToString() { return "Boat"; } } class Program { static void Main(string[] args) { Console.WriteLine(new Boat().ToString()); } } |
# Abstract Vehicle class class Vehicle def initialize if self.class == Vehicle raise "Cannot instantiate an abstract class." end end end class Car < Vehicle def initialize super end def to_s "Car" end end class Boat < Vehicle def initialize super end def to_s "Boat" end end |
Resulting of course in:
C# | Ruby |
![]() |
![]() |
Equivalence and Hash Codes
Ruby has several equality tests which can lead to confusion.6 The basic rule of thumb though is that the "equal?" function should never be overridden as it is used to determine object identity - object A and object B are the same instance. Contrast this with C#, in which a warning message is issued if, when overriding ==, the Equals method is also not overridden. Consider the C# and Ruby implementation for an "Engine" class in which different instances are considered equivalent if the number of cylinders is the same (yes, this is a contrived example):
C# | Ruby |
public class Engine { public int Cylinders { get; set; } public Engine(int numCyl) { Cylinders = numCyl; } public static bool operator ==(Engine a, Engine b) { return a.Cylinders == b.Cylinders; } public static bool operator !=(Engine a, Engine b) { return !(a==b); } public override bool Equals(object obj) { Engine e = obj as Engine; if ((object)e == null) { return false; } return Cylinders == e.Cylinders; } public override int GetHashCode() { return base.GetHashCode() ^ Cylinders.GetHashCode(); } } class Program { static void Main(string[] args) { Engine e1 = new Engine(4); Engine e2 = new Engine(4); Engine e3 = new Engine(6); Console.WriteLine(e1 == e2); Console.WriteLine(e1 == e3); Console.WriteLine(e1.Equals(e2)); Console.WriteLine((object)e1 == (object)e2); } } |
class Engine attr_accessor :numCyl def initialize(numCyl) @numCyl = numCyl end def ==(other) self.class.equal?(other.class) && @numCyl==other.numCyl end def hash super.hash ^ numCyl end end e1 = Engine.new(4) e2 = Engine.new(4) e3 = Engine.new(6) e1==e2 e1==e3 e1.Equal?(e2) |
resulting in the output:
C# | Ruby |
![]() |
![]() |
Notice in C# that in order to perform instance comparison, the instances must be cast to objects so as not to use the overridden == operator, and furthermore, the Equals operator can no longer be used to test for different instances.
Notice in the Ruby code that a test for the same type is made, because in Ruby, we can pass in objects of different types and, if it had a numCyl attribute, the code would happily compare to disparate objects.
Comparability
Let's take a quick look at comparison operators.7 The comparison operators <., <=, >=, > each require a separate override in C# which is much simpler in Ruby:
C# | Ruby |
public class Engine { ... public static bool operator <(Engine a, Engine b) { return a.Cylinders < b.Cylinders; } public static bool operator <=(Engine a, Engine b) { return a.Cylinders <= b.Cylinders; } public static bool operator >(Engine a, Engine b) { return a.Cylinders > b.Cylinders; } public static bool operator >=(Engine a, Engine b) { return a.Cylinders >= b.Cylinders; } ... } |
class Engine include Comparable ... def <=>(other) self.class == other.class ? @numCyl <=> other.numCyl : nil end end |
The Ruby code takes advantage of something called a "mixin" (more on that later) and the <=> operator.
Dispose and Finalizers
Both C# and Ruby use automatic garbage collection rather than requiring the programmer to make explicit de-allocation calls. This is fine until one has to free up unmanaged resources. C# provides a couple mechanisms for doing this, the most common of which is to wrap the code that is using an unmanaged resource in a "using" block--when the block exists, the object's destructor is automatically called, assuming the class implements the IDispose interface, a correct implementation10 in C# and a correct implementation11 of the finalizer and a hackish implementation of dipose (by me, borrowing13 from some example code regarding "using") in Ruby looks like this:
C# | Ruby |
public class DisposeMe : IDisposable { private bool disposed = false; public DisposeMe() { Console.WriteLine("Constructor"); } ~DisposeMe() { Console.WriteLine("Destructor"); Dispose(false); } public void Dispose() { Console.WriteLine("Dispose"); Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { Console.WriteLine("Disposing. disposed = {0}, disposing = {1}", disposed, disposing); if (!disposed) { if (disposing) { Console.WriteLine("Free other managed resources"); } Console.WriteLine("Free unmanaged resources"); disposed = true; } } } |
# global method - needs to be defined only once somewhere def using(object) yield object ensure object.dispose end class DisposeMe @disposed = false def initialize ObjectSpace.define_finalizer(self, self.class.finalize(@disposed)) end # pass in other instance specific params that are needed # to dispose of unmanaged objects. def self.finalize(disposed) proc { if !disposed dispose #pass in other args here else puts "object already disposed" end } end # class method called by GC. Pass in other required instance # variables here to dispose of unmanaged objects. def self.dispose puts "Disposing unmanaged objects" end # instance method called by using. We have access to instance # variables here def dispose self.class.dispose @disposed = false end end |
Note how in the Ruby code we need both a class and an instance method "dispose" because the finalizer's proc cannot reference its own instance--the object will never be collected otherwise.
A simple test program:
C# | Ruby |
class Program { static void Main(string[] args) { Console.WriteLine("Begin"); using (DisposeMe d = new DisposeMe()) { Console.WriteLine("Doing something with DisposeMe"); } Console.WriteLine("End"); } } |
using(DisposeMe.new) do |my_obj| puts "Doing something."; end # simulate GC after using has disposed object DisposeMe.finalize(true).call # simulate GC when object hasn't been disposed DisposeMe.finalize(false).call |
yields (hahaha):
C# | Ruby |
![]() |
![]() |
Property Assignment During Construction
A sometimes useful syntax in C# is to initialize properties during the class instantiation, but not as parameters passed to the constructor. If you want to do this in Ruby, you can simply add a block to the constructor
C# | Ruby |
public class TwoNums { public int A { get; set; } public int B { get; set; } public int Sum { get { return A + B; } } } class Program { static void Main(string[] args) { TwoNums nums = new TwoNums() { A = 1, B = 5 }; Console.WriteLine(nums.Sum); } } |
class Adder attr_accessor :a attr_accessor :b def sum a+b end def initialize(&block) block.call(self) unless block.nil? end end # Example usage: adder = Adder.new do |this| this.a=1; this.b=5 end adder.sum adder2 = Adder.new |
However, this requires that you create constructors specifically with the ability to call a block (an expression). A significantly more complicated15 solution involves redefining the "new" function of a class, which I won't get into here, and requires that the "initializable attributes" be declared so that the new function can determine which arguments are initializer arguments and which are constructor arguments.
Interfaces
A discussion of classes would not be complete without looking at the concept of interfaces in C# and Ruby. In C#, an interface enforces a kind of contract: class C, inheriting from interface I, will implement all the properties and methods defined in I. In Ruby, interfaces are meaningless because methods can be added and removed dynamically from classes and even if they are missing from a class, they can be handled in the "method_missing" hook that Ruby provides.8 The dynamic nature of Ruby makes it impossible to enforce an implementation contract (an interface.)
Multiple Inheritance (mix-ins and modules)
C# does not support multiple inheritance at all. Interfaces are not really a way to implement multiple inheritance because the interface is merely a definition, not an implementation. Ruby does not explicitly support multiple inheritance in a formal syntax (like "Car < Vehicle, OperatorLicense") however the principles behind multiple inheritance (inheriting implementation from multiple source) is achieved with modules and mix-ins9. For example:
require "date" # Abstract Vehicle class class Vehicle def initialize if self.class == Vehicle raise "Cannot instantiate an abstract class." end end end module OperatorLicense attr_accessor :licenseNo attr_accessor :expirationDate end class Car < Vehicle include OperatorLicense def initialize super end def to_s "#{licenseNo} expires #{expirationDate.strftime("%m/%d/%Y")}" end end
By "mixing in" the module "OperatorLicense", we acquire the attributes (aka properties) "licenseNo" and "expirationDate", which we can now use in the Car subclass:
car = Car.new car.licenseNo = "1XYZ30" car.expirationDate=DateTime.new(2016, 12, 31)
resulting in:
This is a powerful way to extend the behavior to Ruby classes, and you will see this used frequently (for example, Comparable used above is a mix-in.)
Fields and Properties
Properties in C# are back by either explicit or implicit fields, which are termed "attributes" in Ruby.
Fields
In the following example, the use of fields is equivalent between C# and Ruby:
C# | Ruby |
public class Person { private string firstName; private string lastName; public Person(string first, string last) { firstName = first; lastName = last; } public string FullName() { return firstName + " " + lastName; } } |
class Person def initialize(first, last) @first_name = first @last_name = last end def full_name @first_name + " " + @last_name end end |
Of course, note that in Ruby one does not need to declare fields.
Properties
Similarly, properties are methods in C# and Ruby. If we avoid the syntactical sugar in both languages, properties are declared thus:
C# | Ruby |
public class Person { private string firstName; private string lastName; public string get_firstName() {return firstName;} public void set_firstName(string name) {firstName=name;} public string get_lastName() { return lastName; } public void set_lastName(string name) { lastName = name; } public Person(string first, string last) { firstName = first; lastName = last; } public string FullName() { return firstName + " " + lastName; } } |
class Person def first_name @first_name end def first_name=(name) @first_name=name end def last_name @last_name end def last_name=(name) @last_name=name end def initialize(first, last) @first_name = first @last_name = last end def full_name @first_name + " " + @last_name end end |
This is obviously not the syntax typically used in either language. What we normally see is something more like:
C# | Ruby |
public class Person { public string FirstName { get; set; } public string LastName { get; set; } public Person(string first, string last) { FirstName = first; LastName = last; } public string FullName() { return FirstName + " " + LastName; } } |
class Person attr_accessor :first_name attr_accessor :last_name def initialize(first, last) @first_name = first @last_name = last end def full_name @first_name + " " + @last_name end end |
The usage is what we would expect:
C# | Ruby |
Person p = new Person("Marc", "Clifton"); Console.WriteLine(p.FullName()); p.FirstName = "Ian"; Console.WriteLine(p.FullName()); |
p = Person.new("Marc", "Clifton") p.full_name p.first_name="Ian" p.full_name |
Read-Only Properties
A common form in C# is to demark the setter as protected so that it is not writeable (this is different from a "readonly" attribute). A similar concept exists in Ruby:
C# | Ruby |
public string FirstName { get; protected set; } public string LastName { get; protected set; } |
attr_reader :first_name attr_reader :last_name |
And in Ruby, attributes with the same access level can be grouped together, for example:
attr_reader :first_name, :last_name
Also, in Ruby, there is an "attr_writer", equivalent in C# to the following:
C# | Ruby |
public string FirstName { protected get; set; } public string LastName { protected get; set; } |
attr_writer :first_name attr_writer :last_name |
And in fact, attr_accessor in Ruby is the equivalent of declaring attr_reader and attr_writer.
Read Only Fields
The C# readonly attribute can be implemented to prevent assignment to fields. There is no equivalent in Ruby -- do not confuse the "freeze" function with C#'s readonly attribute. Ruby's freeze function prevents an object from being modified, it does not prevent the same object from being assigned a new value.
C# | Ruby |
public class ReadOnlyExample { // Can be initialized in the declaration. public readonly int SomeValue = 13; public ReadOnlyExample() { // Can be assigned in the constructor SomeValue = 15; } public void AttemptToModify() { // but no further assignments are allowed. SomeValue = 3; } } |
class ReadOnlyExample attr_accessor :some_value def initialize @some_value = 15 @some_value.freeze end def attempt_to_modify @some_value = 3 end end a = ReadOnlyExample.new a.attempt_to_modify a.some_value |
Resulting in the compiler error in C#:
error CS0191: A readonly field cannot be assigned to (except in a constructor or a variable initializer)
but results in a perfectly valid execution in Ruby:
Static Properties (Class Properties)
The above describes "instance properties" and in both languages there is support for static properties, otherwise known as class variables in Ruby. In the following example, I've added a static property to count the instantiations of the Person type:
C# | Ruby |
public class Person { public static int TotalPeople { get; set; } public string FirstName { protected get; set; } public string LastName { protected get; set; } public Person(string first, string last) { FirstName = first; LastName = last; ++TotalPeople; } public string FullName() { return FirstName + " " + LastName; } } class Program { static void Main(string[] args) { Person p1 = new Person("Marc", "Clifton"); Person p2 = new Person("Ian", "Clifton"); Console.WriteLine(Person.TotalPeople); } } |
class Person @@total_people = 0 attr_accessor :first_name, :last_name def self.total_people @@total_people end def initialize(first, last) @first_name = first @last_name = last @@total_people += 1 end def full_name @first_name + " " + @last_name end end p1 = Person.new("Marc", "Clifton") p2 = Person.new("Ian", "Clifton") Person.total_people #also Person::total_people |
Note the "self." in the function "total_people", which indicates that the function is association with the class Person rather than the instance p1 and p2. The Ruby code could also have been written as:
def Person.total_people @@total_people end
However, "self" is becoming more in vogue. Also, consider this syntax:
class Person class << self attr_accessor :total_people end def initialize(first, last) @first_name = first @last_name = last Person.total_people ||= 0 Person.total_people += 1 end def full_name @first_name + " " + @last_name end end p1 = Person.new("Marc", "Clifton") p2 = Person.new("Ian", "Clifton") Person.total_people
Here, the syntax "class << self" is a singleton declaration, specifying that attributes and methods belong to the class definition rather than the class instance and can be accessed with the "Person." construct ("self." no longer works).
Writing Your Own Accessor
And in Ruby, we could just write our own "attr_static_accessor"12 by adding the definition for "attr_static_accessor" to the Class type:
class Class def attr_static_accessor(class_name, *args) args.each do |arg| self.class_eval("def #{class_name}.#{arg};@@#{arg};end") self.class_eval("def #{class_name}.#{arg}=(val);@@#{arg}=val;end") end end end class Person attr_static_accessor(:Person, :total_people) def initialize(first, last) @first_name = first @last_name = last @@total_people ||= 0 @@total_people += 1 end def full_name @first_name + " " + @last_name end end p1 = Person.new("Marc", "Clifton") p2 = Person.new("Ian", "Clifton") Person.total_people
which results in the expected behavior:
Computational Getters and Setters
In C#, computations are often placed into property getters and setters. Obviously, in Ruby, one can just write the functions:
C# | Ruby |
public class Person { public string FirstName { get; protected set; } public string LastName { get; protected set; } public string FullName { get { return FirstName + " " + LastName; } set { string[] names = value.Split(' '); FirstName = names[0]; LastName = names[1]; } } } class Program { static void Main(string[] args) { Person p = new Person() {FullName = "Marc Clifton"}; Console.WriteLine(p.FirstName); Console.WriteLine(p.LastName); Console.WriteLine(p.FullName); } } |
class Person attr_accessor :first_name, :last_name def full_name=(value) names = value.split(" ") @first_name=names[0] @last_name=names[1] end def full_name @first_name + " " + @last_name end end p = Person.new p.full_name = "Marc Clifton" p.first_name p.last_name p.full_name |
Resulting in:
C# | Ruby |
![]() |
![]() |
However, if you want to get fancy, one can do the equivalent in Ruby by passing the expression in as a string and using custom attribute accessors. First we define a "attr_computed_accessor" which will be available to all classes.
class Class def attr_computational_accessor(props) props.each { |prop, fnc| if fnc.key?(:getter) self.class_eval("def #{prop};#{fnc[:getter]};end") end if fnc.key?(:setter) self.class_eval("def #{prop}=(value);#{fnc[:setter]};end") end } end end
Now we can write a test class:
class ComputationalProperty #expose attributes set by computation attr_accessor :first_name attr_accessor :last_name attr_computational_accessor(:full_name => { :getter => %{@first_name + " " + @last_name}, :setter => %{ names=value.split(" ") @first_name = names[0] @last_name = names[1] } }) end
Note that the above code, while it looks like a lambda expression, actually isn't -- it's a string being passed to the class_eval function.
Here's the result:
If you want to use lambda expressions, you have to pass in the context14. This is how the attribute is defined:
class Class def attr_computational_accessor(props) props.each { |prop, fnc| if fnc.key?(:getter) self.class_eval do define_method prop do instance_eval do fnc[:getter].call(self) end end end end if fnc.key?(:setter) self.class_eval do define_method "#{prop}=".to_sym do |value| instance_eval do fnc[:setter].call(self, value) end end end end } end end
requiring that the context be specified to access instance attributes and methods, which I represent with the parameter "this":
class ComputationalProperty #exposes attributes set by computation attr_accessor :first_name attr_accessor :last_name attr_computational_accessor(:full_name => { :getter => lambda {|this| this.first_name + " " + this.last_name}, :setter => lambda {|this, value| names = value.split(" ") this.first_name = names[0] this.last_name = names[1] } }) end
Here's one more usage example, illustrating just a getter:
class AnotherComputer attr_accessor :a attr_accessor :b attr_computational_accessor(:sum => {:getter => lambda {|this| this.a + this.b}}) end
Example usage:
Events
Another common practice in C# is to provide events for when property values change. For example, here we have a class that fires a "changing" event, providing the event handler with both the old and new values. As a comparison with Ruby, I am leveraging the Ruby gem "ruby_events"16:
C# | Ruby |
public class PropertyChangingEventArgs<T> : EventArgs { public T OldValue { get; set; } public T NewValue { get; set; } } public delegate void PropertyChangingDlgt<T>(object sender, PropertyChangingEventArgs<T> args); public class Things { public event PropertyChangingDlgt<int> AChanging; private int a; public int A { get { return a; } set { if (a != value) { if (AChanging != null) { AChanging(this, new PropertyChangingEventArgs<int>() { OldValue = a, NewValue = value }); } a = value; } } } } |
require 'rubygems' require 'ruby_events' class Things def A @a end def A=(value) if (value != @a) events.fire(:a_changing, self, @a, value) end @a=value end end |
Note that I just broke the naming convention style in the Ruby code -- the "A" function should actually be "a".
And to test:
C# | Ruby |
class Program { static void Main(string[] args) { Things things = new Things() { A = 2 }; things.AChanging += (sender, changeArgs) => { Console.WriteLine("Old Value = {0}, New Value = {1}", changeArgs.OldValue, changeArgs.NewValue); }; things.A=5; } } |
![]() |
That was easy enough. There are many other ways to do this as well17 but one of the interesting features of the ruby_events gem is that you can inject callbacks into existing class methods. For example, the property setter doesn't need to explicitly raise an event:
class OtherThings attr_accessor :b end o = OtherThings.new o.b = 2 o.events.fire_on_method(:b=.to_sym, :injected) o.events.listen(:injected) do |event_data| puts "b is now " + event_data.to_s end o.b = 5 b is now 5 => 5
Now what could be cooler than that!
Methods
We've already seen a variety of methods--property getters, setters, and some simple functions. There are a couple other concepts to cover here.
Static Methods
Static methods are prefixed either with "self." or with the class name. For example, these are equivalent:
class StaticMethods def self.first_static_method puts "first" end def StaticMethods.second_static_method puts "second" end end
Usage is just like C#:
Abstract Methods
If you want to enforce abstract behavior you can define a class-level function that raises an exception if accidentally called, thus enforcing implementation in the derived class (of course, because Ruby is interpreted, this occurs at runtime.)
class Class def abstract(*methods) methods.each do |m| define_method(m) {raise "Abstract method #{m} called."} end end end
which is used thus:
class AbstractMethods abstract :A1, :A2 end class ConcreteMethods < AbstractMethods def A1 puts "A1" end # oops, didn't implement A2 end
Resulting in the following output:
Method Missing
"All RubyEvents functionality is just an object.events call away."16
Yes, methods are actually messages, which opens the door to creating new behaviors when a method to a class is missing. So let's use the method missing feature to convert Roman numerals to their Arabic equivalent (heavily borrowed code 19):
class RomanConverter @@data = [ ["M" , 1000], ["CM" , 900], ["D" , 500], ["CD" , 400], ["C" , 100], ["XC" , 90], ["L" , 50], ["XL" , 40], ["X" , 10], ["IX" , 9], ["V" , 5], ["IV" , 4], ["I" , 1] ] def self.to_arabic(rom) result = 0 for key, value in @data while rom.index(key) == 0 result += value rom.slice!(key) end end result end def self.method_missing(sym, *args) # no validation! to_arabic(sym.to_s) end end
Now we can use Roman numerals as method names and return the Arabic number:
Testing
In essence, the problem is that, "if it walks like a duck and quacks like a duck", it could be a dragon doing a duck impersonation. You may not always want to let dragons into a pond, even if they can impersonate a duck.
Proponents of duck typing, such as Guido van Rossum, argue that the issue is handled by testing, and the necessary knowledge of the codebase required to maintain it. (Duck Typing)
If you read through any of the sections in this article, you will probably be very quickly struck by the flexibility of Ruby and the fact that, being a duck-typed language, there is no compile-time type checking -- if there are type violations, missing implementation, and so forth, those may be discovered when you run the program--if the dragon can quack, it's still not a duck. Furthermore, the amazing meta-programming capabilities18 of Ruby also won't reveal issues until runtime. For this reason, the need for writing tests cannot be overstated. As you develop a Ruby application, tests should be incrementally and immediately added for each new behavior and change to existing behaviors. If you fail to do this, you will most likely encounter runtime errors months later because a particular code branch broke that relied on something that no longer exists or works differently. Two popular testing tools are RSpec and Cucumber.
References
1 -
http://rubylearning.com/satishtalim/ruby_access_control.html
2 -
http://stackoverflow.com/questions/512466/how-to-implement-an-abstract-class-in-ruby
3 -
http://en.wikibooks.org/wiki/Ruby_Programming/Syntax/Classes
4 -
http://blog.rubybestpractices.com/posts/rklemme/018-Complete_Class.html
5 -
http://openmymind.net/2010/6/25/Learning-Ruby-class-self/
6 -
http://stackoverflow.com/questions/7156955/whats-the-difference-between-equal-eql-and
7 -
http://blog.rubybestpractices.com/posts/rklemme/018-Complete_Class.html
8 - http://briancarper.net/blog/226/
9 -
http://www.ruby-doc.org/docs/ProgrammingRuby/html/tut_modules.html
10 -
http://lostechies.com/chrispatterson/2012/11/29/idisposable-done-right/
11 -
http://www.mikeperham.com/2010/02/24/the-trouble-with-ruby-finalizers/
12 -
http://mikeyhogarth.wordpress.com/2011/12/01/creating-your-own-attr_accessor-in-ruby/
13 -
http://usrbinruby.net/ruby-general/2010-01/msg01369.html
14 -
http://stackoverflow.com/questions/7470508/ruby-lambda-context
15 -
http://stackoverflow.com/questions/1077949/best-way-to-abstract-initializing-attributes
16 -
https://github.com/nathankleyn/ruby_events
17 -
http://thatextramile.be/blog/2010/08/using-c-style-events-in-ruby
18 -
http://yehudakatz.com/2009/11/15/metaprogramming-in-ruby-its-all-about-the-self/
19 -
http://www.rubyquiz.com/quiz22.html