Home > value types, yield return > Generic Value Object With Yield Return

Generic Value Object With Yield Return

Inspired by this and this, here’s my version.

Jan Van Ryswyck is right to use static reflection, and to do that he "registers" the properties in a list. On the other hand, assuming you’d have a lot of these objects building up in the GC, it might prove important to reduce the number of ancillary allocations going on, so it may be better to avoid creating a list of the properties in every single instance.

It’s not really necessary to have the full information about the properties via reflection; we only need the values. Also it’s a pain to have to distinguish between fields and properties. Finally, we are really doing operations on a sequence of values, which can be expressed very neatly with Linq operators, and specified in the derived class with an iterator method.

So here’s what I came up with:

public abstract class ValueObject<T> : IEquatable<T>
    where T : ValueObject<T>
{
    protected abstract IEnumerable<object> Reflect();

    public override bool Equals(Object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (obj.GetType() != GetType()) return false;
        return Equals(obj as T);
    }

    public bool Equals(T other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return Reflect().SequenceEqual(other.Reflect());
    }

    public override int GetHashCode()
    {
        return Reflect().Aggregate(36, (hashCode, value) => value == null ? 
                                hashCode : hashCode ^ value.GetHashCode());
    }

    public override string ToString()
    {
        return "{ " + (string) Reflect().Aggregate((l, r) => l + ", " + r) + " }";
    }
}

First off, it’s a lot shorter! Aside from the standard ReferenceEquals checks, every method is a one-liner, a return of a single expression. Check out how SequenceEqual does so much work for us. And Aggregate is designed for turning a sequence into one value, which is exactly what GetHashCode and ToString are all about.

This is all possible because we’re treating the properties or fields as just a sequence of values, obtained from the Reflect method, which the derived class has to supply.

(You could easily add operator== and operator!= of course. Also in case you’re wondering about the way ToString appears not to check for null, actually it does, because one odd thing about .NET is that string concatenation can cope with null strings.)

Secondly, the way you use it is pretty neat as well:

public class Person : ValueObject<Person>
{
    public int Age { get; set; }
    public string Name { get; set; }

    protected override IEnumerable<object> Reflect()
    {
        yield return Age;
        yield return Name;
    }
}

It wouldn’t matter if I yielded the values of fields or properties, and there’s no need for expression-lambda trickery to get a PropertyInfo.

If you’re unfamiliar with how iterator methods work, you may be wondering, what if I have twenty fields and the first fields of two instances are unequal, isn’t this going to waste time comparing the other nineteen fields? No, because SequenceEqual will stop iterating as soon as it finds an unequal element pair, and iterator methods are interruptible.

(Note that if you need this to work on .NET 2.0, you can grab Jared Parsons’ BCL Extras library to get the necessary sequence operations. If you’re using the C# 2.0 compiler you just need to rejig the method call syntax to avoid using them as extension methods. Iterator methods were already available in 2.0, so nothing here is dependent on 3.0.)

About these ads
  1. williamwsmithiii
    June 9, 2009 at 8:35 pm

    Using extension methods works in .NET 2.0 as well:
    http://www.codethinked.com/post/2008/02/Using-Extension-Methods-in-net-20.aspx

    Also another LINQ implementation:
    http://www.albahari.com/nutshell/linqbridge.aspx

  2. Daniel Earwicker
    June 10, 2009 at 7:48 am

    Not if you're using the C# 2.0 compiler.

  3. Paul Westcott
    July 10, 2009 at 5:23 am

    G’day Daniel,

    (Hopefully this message isn’t too long, and gets cut. If so, just send me an email, and I’ll repeat it there)

    I wrote some stuff a little while back that is kind of similar to the Generic Value Object, but doesn’t use a base class.

    The project is called Essence ( http://essence.codeplex.com/ ), and it uses the System.Linq.Expression libraries to generate (based on attributes) standard representations of Equals/GetHashCode/CompareTo/ToString, as well as being able to create IEqualityComparer and IComparer classes based on an argument list. (I also have some further ideas, but would like to get some community feedback before continuing too much further.)

    (What this means is that it’s almost as fast as being handwritten – the main one where it isn’t is the CompareTo(); cause the Linq.Expressions doesn’t have the concept of a variable in the 3.5 release – so you have to call CompareTo() on the underlying object twice when you don’t get a match. Using the DLR extensions to Linq.Expressions solves this. I suppose I could have used the emit il, but I wasn’t that inspired at the time.)

    It’s quite a simple idea, but I haven’t seen it done before.

    Now the thing is, I kind of lost interest in polishing it (which would have included writing an article for codeproject, documenting some of the code, or the like), but I might be persuaded to do so if you feel it would be something of interest.

    (The codeplex site doesn’t have a downloadable package; just go to the source and grab that – oh, it’s written in f# (although all the test code is in c#) as that was the thing I was interested in learning.)

    Anyway, here is are some sample c# examples from my test cases.

    // ——————————————————————–
    // USING MY ESSENCE LIBRARY:
    // ——————————————————————–
    [EssenceClass(UseIn = EssenceFunctions.All)]
    public class TestEssence : IEquatable, IComparable
    {
    [Essence(Order=0, Format="i={0},")] public int MyInt { get; set; }
    [Essence(Order=1, Format="s={0},")] public string MyString { get; set; }
    [Essence(Order=2, Format="d={0:yyyy-MM-dd}")] public DateTime MyDateTime { get; set; }

    public override int GetHashCode() { return Essence.GetHashCodeStatic(this); }
    public override string ToString() { return Essence.ToStringStatic(this); }
    public int CompareTo(TestEssence other) { return Essence.CompareToStatic(this, other); }

    public static bool operator ==(TestEssence lhs, TestEssence rhs) { return Essence.EqualsStatic(lhs, rhs); }
    public override bool Equals(object obj) { return this == (TestEssence)obj; }
    public bool Equals(TestEssence other) { return this == other; }
    public static bool operator !=(TestEssence lhs, TestEssence rhs) { return !(lhs == rhs); }
    }

    // ——————————————————————–
    // EQUIVALENT HAND WRITTEN CODE:
    // ——————————————————————–
    public class TestManual
    {
    public int MyInt;
    public string MyString;
    public DateTime MyDateTime;

    public override int GetHashCode()
    {
    var x = MyInt.GetHashCode();
    x *= Essence.HashCodeMultiplier;
    x ^= (MyString == null) ? 0 : MyString.GetHashCode();
    x *= Essence.HashCodeMultiplier;
    x ^= MyDateTime.GetHashCode();
    return x;
    }

    public static bool operator ==(TestManual lhs, TestManual rhs)
    {
    if (ReferenceEquals(lhs, null))
    {
    if (ReferenceEquals(rhs, null))
    return true;
    return false;
    }
    if (ReferenceEquals(rhs, null))
    return false;
    if (ReferenceEquals(lhs, rhs))
    return true;
    if (typeof(TestManual) != rhs.GetType())
    return false;

    return lhs.MyInt == rhs.MyInt
    && lhs.MyString == rhs.MyString
    && lhs.MyDateTime == rhs.MyDateTime;
    }

    public override bool Equals(object obj) { return this == obj as TestManual; }
    public bool Equals(TestManual other) { return this == other; }
    public static bool operator !=(TestManual lhs, TestManual rhs) { return !(lhs == rhs); }

    public override string ToString()
    {
    if (MyString == null)
    return string.Format(“i={0},d={1:yyyy-MM-dd}”, MyInt, MyDateTime);

    return string.Format(“i={0},s={1},d={2:yyyy-MM-dd}”, MyInt, MyString, MyDateTime);
    }

    public int CompareTo(TestManual other)
    {
    if (other == null)
    return 1;
    if (ReferenceEquals(this, other))
    return 0;

    int result = 0;

    result = MyInt.CompareTo(other.MyInt);
    if (result != 0) return result;
    result = MyString.CompareTo(other.MyString);
    if (result != 0) return result;
    result = MyDateTime.CompareTo(other.MyDateTime);
    if (result != 0) return result;

    return result;
    }
    }

    // ——————————————————————–
    // ——————————————————————–
    // ALTERNATIVE USAGE
    // ——————————————————————–
    // ——————————————————————–

    class Simple
    {
    public Simple(int value) { Value1 = value; }
    public Simple(int value1, int value2) { Value1 = value1; Value2 = value2; }
    public readonly int Value1;
    public readonly int Value2;
    }

    [Test]
    public void TestReverseForwardString()
    {
    var _11 = new Simple(1, 1);
    var _12 = new Simple(1, 2);
    var _21 = new Simple(2, 1);
    var _22 = new Simple(2, 2);

    var items = new[] { _11, _12, _21, _22 };
    var reverseComparer = Essence.CreateComparer(“-Value1″, “Value2″);

    Array.Sort(items, reverseComparer);

    Assert.AreSame(_21, items[0]);
    Assert.AreSame(_22, items[1]);
    Assert.AreSame(_11, items[2]);
    Assert.AreSame(_12, items[3]);
    }

    [Test]
    public void TestReverseForwardLambda()
    {
    var _11 = new Simple(1, 1);
    var _12 = new Simple(1, 2);
    var _21 = new Simple(2, 1);
    var _22 = new Simple(2, 2);

    var items = new[] { _11, _12, _21, _22 };
    var reverseComparer = Essence.CreateComparer(x => x.Action.ReverseNext, x => x.Member.Value1, x => x.Member.Value2);

    Array.Sort(items, reverseComparer);

    Assert.AreSame(_21, items[0]);
    Assert.AreSame(_22, items[1]);
    Assert.AreSame(_11, items[2]);
    Assert.AreSame(_12, items[3]);
    }

    Have fun!

    Paul.

  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: