(Apologies for the formatting of the tables - I'm working on improving the side-by-side comparisons.)
Contents
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".
"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.
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
|
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
|
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#:
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
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.
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:
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.
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:
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.
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:
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:
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.
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.
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 |
|
|
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.
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.)
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.)
Properties in C# are back by either explicit or implicit fields, which are
termed "attributes" in Ruby.
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.
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
|
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.
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
{
public readonly int SomeValue = 13;
public ReadOnlyExample()
{
SomeValue = 15;
}
public void AttemptToModify()
{
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:
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).
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:
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:
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:
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!
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 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#:
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:
"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:
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.
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