I Dream Of Whidbey: Generics and Anonymous Methods
We (the team at iP3) are implementing an object model using Whidbey at the moment, and we have been copying and pasting a lot of code lately which is one of the things that I try to minimise as much as possible. One method that we have copied a lot is a simple (and un-optimised) ‘add’ method that helps maintain the many side of a one-to-many relationship.
After creating a method about a week ago that used Generics to allow greater code reuse, I thought that I might be able to use a combination of Generics and Anonymous Methods to allow even more code reuse.
My goal was to create an ‘add’ method that used Generics and Anonymous Methods and could sit in an external class.
The results are below; ClassA and ClassB are the classes that have the relationship, RelationshipHelper is the class with the add method, DelegateTests is the test fixture I created to support me while I refactored the methods for about an hour ;).
I had one stumbling point during this when trying to set the Contains array on ClassA. It would compile just fine, but would fail the test (thank goodness I had a quick and simple test) with a NullReferenceException. It turns out that the anonymous method needed to call a method (yes, even just a private method) to set the _contains member variable. Weird…
It is interesting to note that the standard method size is 266 characters and the much less readable method that used Anonymous Methods and Generics is 227 characters. I think we’ll have to think about whether we want to encourage readability or code reuse with this. I’m leaning towards the standard method because of the readability, but it is great to have implemented this once so we can use it again if a code reuse opportunity arises in the future.
Enjoy!
ClassA.cs
using System;
using System.Collections.Generic;
namespace Model.Common
{
/// <summary>
/// Example for Generics and Delegates
/// ClassA contains many ClassBs
/// ClassB is contained in one ClassA
/// </summary>
public class ClassA
{
private ClassB[] _contains;
public ClassB[] Contains
{
get
{
return _contains;
}
}
private void SetContains(ClassB[] objects)
{
_contains = objects;
}
public ClassA()
{
}
/// <summary>Standard add method</summary>
public void AddClassB1(ClassB instanceOfB)
{
List<ClassB> list = new List<ClassB>();
if (this.Contains != null)
{
foreach(ClassB item in this.Contains)
{
list.Add(item);
}
}
if (!list.Contains(instanceOfB))
{
list.Add(instanceOfB);
this._contains = list.ToArray();
instanceOfB.ContainedIn = this;
}
}
/// <summary>Add method using Generics and Anonymous Methods</summary>
public void AddClassB2(ClassB instanceOfB)
{
RelationshipHelper.AddToMany<ClassB>(
instanceOfB,
delegate()
{return this.Contains;},
delegate(ClassB[] classBs)
{this.SetContains(classBs);},
delegate(ClassB classB)
{classB.ContainedIn = this;});
}
}
}
ClassB.cs
namespace Model.Common
{
public class ClassB
{
private ClassA _containedIn = null;
public ClassA ContainedIn
{
get { return _containedIn; }
set
{
if (_containedIn != value)
{
_containedIn = value;
}
}
}
public ClassB()
{
}
}
}
RelationshipHelper.cs
using System;
using System.Collections.Generic;
namespace Model.Common
{
/// <summary>
/// Summary description for RelationshipHelper.
/// </summary>
public class RelationshipHelper
{
public delegate T[] GetManyArray<T>();
public delegate void SetManyArray<T>(T[] objects);
public delegate void SetSingleSide<T>(T instance);
public static void AddToMany<T>(
T instanceToAdd,
GetManyArray<T> getManyArray,
SetManyArray<T> setManyArray,
SetSingleSide<T> setSingle)
{
List<T> list = new List<T>();
if (getManyArray() != null)
{
foreach (object item in getManyArray())
{
list.Add((T)item);
}
}
if (!list.Contains(instanceToAdd))
{
list.Add(instanceToAdd);
setManyArray(list.ToArray() as T[]);
setSingle((T)instanceToAdd);
}
}
}
}
DelegateTests.cs
using System;
using NUnit;
using NUnit.Framework;
using Model.Common;
namespace UnitTests.ModelTests
{
[TestFixture]
public class DelegateTests
{
[Test]
public void TestStandardAssociationMethod()
{
ClassA a = new ClassA();
ClassB b1 = new ClassB();
a.AddClassB1(b1);
ClassB b2 = new ClassB();
a.AddClassB1(b2);
Assertion.AssertEquals(2, a.Contains.Length);
Assertion.AssertEquals(a, b1.ContainedIn);
Assertion.AssertEquals(a, b2.ContainedIn);
}
[Test]
public void TestDelegateAssociationMethod()
{
ClassA a = new ClassA();
ClassB b1 = new ClassB();
a.AddClassB2(b1);
ClassB b2 = new ClassB();
a.AddClassB2(b2);
Assertion.AssertEquals(2, a.Contains.Length);
Assertion.AssertEquals(a, b1.ContainedIn);
Assertion.AssertEquals(a, b2.ContainedIn);
}
}
}
Comments
- Anonymous
March 15, 2004
You have been Taken Out! Thanks for the post. - Anonymous
March 16, 2004
Why do you need to use delegates? All of the objects your delegates return already exist and the delegates always get called anyway, so it is not lazy creation! Is it to limit what RelationshipHelper can do with the collections? Probably not, because any client who does ClassA::Contains can mutate the array (insert nulls, insert duplicate clients, etc.) Are you aiming for CLS compliance? Probably not-- RelationshipHelper exposes generic types. I think a much simpler and better approach would be to make _contains an IList<ClassB>; burn a few words of memory and create an empty list in the constructor instead of polluting your code with if _contains != null; return a defensive copy in ClassA::Contains (see Effective Java p. 122) to protect your invariants; and go for the obvious code in ClassA::AddClassB1. There are some other dubious things here: Is being able to contain nulls by design? If not, ClassA::AddClassB? should throw a NullReferenceException. And since ClassB should only appear in one ClassA, I would either make ClassB::ContainedIn into a set-once property, or have it remove itself from its previous container. Just my $0.02. - Anonymous
March 16, 2004
Thanks for your comments Dominic.
The use of delegates was dreamed up since there are a number of classes that have to implement this relationship.
We had thought that maybe we could increase code reuse if we used anonymous methods i.e. we wouldn't have to copy and paste the "copy member array to list, add instance to list, and copy list to member array" code since we could have that sit somewhere else and just create delegates for the SetSingle, SetArray, GetArray parts.
Making _contains an IList<ClassB> was something we had talked about, but I wanted to do this as a prototype first to see if it was viable.
I hadn't thought about the containing of nulls. A good point, I'll look out for this in the future.
Thanks
- Chris - Anonymous
March 16, 2004
Chris,
something else that excites me about Generics and Anonymous Methods:
The ability to do Smalltalk like select/detect/collect methods on the Collection classes (Ofcourse, its not smalltalk, so you can't directly modify the library classes).
How about this:
public delegate bool IsTrueBlock(T obj);
public class SmalltalkList<T> : ArrayList<T>
{
public IList<T> Select(IsTrueBlock block)
{
IList<T> list = new ArrayList<T>();
foreach (<T> o in this)
{
if (block(o))
list.Add(o);
}
return list;
}
public <T> Detect(IsTrueBlock block)
{
foreach (<T> o in this)
{
if (block(o))
return o;
}
return null;
}
}
SmalltalkList<Person> list = new SmalltalkList<Person>();
list.Add(new Person("John", 22));
list.Add(new Person("Chris", 25));
list.Add(new Person("Sean", 23));
list.Add(new Person("Amy", 22));
list.Add(new Person("Frank", 29));
list.Add(new Person("Adrian", 41));
IList<Person> overForty = list.Select(delegate(Person p) { return p.Age > 40; });
// should only have Adrian in the list
Person ageEquals = list.Detect(delegate(Person p) { return p.Age == 22; });
// should be John (Detect returns first instance that matches)
I don't even know if what I have written will compile (I have no access to whidbey). I'm simply theorizing. But if it does work, writing generic selection methods on collections will be quite nice.
It makes me want a language syntax for lambda functions rather than the current state.. ie:
rather than
Select(delegate(Person p) { return p.Age > 10; });
Have somehting like
Select([ Person p | return p.Age > 10; ]); // or is too close to smalltalk! ;))
I don't see why the C# Compiler couldn't turn that syntax into MSIL. - Anonymous
March 17, 2004
Re: Smalltalk and lambda expressions: One difficulty is that delegates have name, not structural, equality. Therefore delegate int A(int x) and delegate int B(int x) are different, incompatible types. This decision makes sense if you think delegates have an implied 'contract'. On the other hand, lambda expressions are really cool and useful. I think to have any hope of getting this in the CLR someone should start to incrementally modify Rotor: Firstly, to modify the CLI to implement a subtyping rule for delegates, which is the moral equivalent of having a first-class function type; then to modify C#. - Anonymous
March 17, 2004
Dominic,
Yes, its also why you can't create two delegates with the same name.
However, in the context that the delegate is used, it still has a declared Type. And the type is the name of the delegate.
IE:
public delegate bool IsTrue(Person p);
Correct me if I'm wrong, but this should be theoretically (with langauge extension):
IsTrue block = [ Person p | return p.Age > 10; ];
you can ascertain the ecact nature of the lambda base don its declared type vs the interface contract yes?
Same as per a method declaration:
public void Select(IsTrue block) { ... }
...
list.Select([ Person p | return p.Age > 10; ]);
the contract implies that the piece of code being passed in, is a delegate which matches the contract specified by the delegate definition of: public delegate bool IsTrue(Person p);
you say:
"delegate int A(int x) and delegate int B(int x) are different, incompatible types"
show me the whidbey code where you could pass in one delegate of type A, to a method that is expecting a delegate of type B:
A methodA = delegate(int x) { return x; };
B methodB = delegate(int x) { return x; };
void Select(B method) { }
would throw a compilation error if you tried to pass an A delegate to it.
Now look at the following code:
A methodA = [int x | return x; ];
B methodB = [int x | return x; ];
sure, they look the same, but they ARE different types.
So I'm only arguing for a syntactic change. The rest of the support for what I want is lready there in whidbey, its merely the way a an anonymous delegate/method is declared in C# is what I'm asking for - Anonymous
March 17, 2004
I should have said
"Yes, its also why you can't create two delegates with the same name but different parameters".