5
\$\begingroup\$

I have an UpdateId class which represents some kind of version (like AssemblyVersion). UpdateId has three int values (listed in their compare priorities order): Year, ReleaseNumber and Revision.

How can I make my CompareTo method better? It looks clumpy to me.

public int CompareTo(object obj)
{
 if(obj == null) return 1;
 var updateId = obj as UpdateId;
 if (updateId != null)
 {
 int yearCompare = this.Year.CompareTo(updateId.Year);
 if (yearCompare == 0)
 {
 int releaseNumberCompare = this.ReleaseNumber.CompareTo(updateId.ReleaseNumber);
 if (releaseNumberCompare == 0)
 {
 return this.Revision.CompareTo(updateId.Revision);
 }
 return releaseNumberCompare;
 }
 return yearCompare;
 }
 else
 {
 throw new ArgumentException("Object is not an UpdateId");
 }
}
200_success
145k22 gold badges190 silver badges478 bronze badges
asked Jul 9, 2015 at 9:23
\$\endgroup\$

3 Answers 3

4
\$\begingroup\$

You could change it to reduce the "arrow style" like so

public int CompareTo(object obj)
{
 if(obj == null) { return 1; }
 var updateId = obj as UpdateId;
 if (updateId == null) 
 {
 throw new ArgumentException("Object is not an UpdateId");
 }
 int compareResult = this.Year.CompareTo(updateId.Year);
 if (compareResult != 0) { return compareResult; }
 compareResult = this.ReleaseNumber.CompareTo(updateId.ReleaseNumber);
 if (compareResult != 0) { return compareResult; }
 return this.Revision.CompareTo(updateId.Revision);
}

which will save you some horizontal spacing and some not needed variables, because they all meant the same so I changed them to compareResult.

answered Jul 9, 2015 at 9:36
\$\endgroup\$
4
\$\begingroup\$

As always, before you ask "how do I do this?", you should ask "has anybody already done this for me?"

public int CompareTo(object obj)
{
 if(obj == null) { return 1; }
 var other = obj as UpdateId;
 if(other == null) 
 { 
 throw new ArgumentException("Object is not an UpdateId", "obj"); 
 }
 return this.AsTuple().CompareTo(other.AsTuple());
}
private IComparable AsTuple()
{
 return Tuple.Create(Year, ReleaseNumber, Revision);
}

Note that this does have a potential performance overhead, so you should check anywhere that UpdateId is being used in a tight loop (which will generally be if it's being sorted). One potential performance improvement- if the Year, ReleaseNumber and Revision are immutable- would be to store the tuple in a private field so it does not need to be regenerated each time.


Note that as well as pushing the logic to an existing .NET class rather than trying to do it ourselves, we also:

  • Invert the ArgumentException check. For some reason you do this for the null check but not the invalid argument type check or the ones that come later. As you can see from Heslacher's answer, this inversion makes the method much cleaner.
  • We remove the redundant "this" keywords. Actually I did leave one in in the last line of CompareTo because I think it makes it read better. But usually, they just clutter things up if they're not needed.

Realistically, it's unlikely that you're ever going to write anything that consumes IComparable directly. Instead you're probably going to have .NET library methods do it for you. For this reason, consider using explicit interface implementation so that CompareTo doesn't clutter up your class's public interface (e.g. in IntelliSense) unnecessarily.

If you do want to be able to compare versions individually rather than as a sort operation (e.g. "Is the current version installed less than the latest version?"), you can implement the comparison operators as RobH suggests

answered Jul 9, 2015 at 10:07
\$\endgroup\$
4
  • \$\begingroup\$ Similar to your answer recently where you used a Tuple to generate hash codes, I'm not sure if I love or hate this. IComparable comes up a lot in loops and all of the extra Tuples could make the GC work much harder than it has to... Which could not matter a jot in real world workloads... \$\endgroup\$ Commented Jul 9, 2015 at 10:43
  • \$\begingroup\$ @RobH In the other answer I mentioned explicitly that you should check for performance issues. Maybe I should mention it in this one too. You could always have the Tuple in a private field instead of regenerated each time, but it's the usual considerations about when to optimize performance \$\endgroup\$ Commented Jul 9, 2015 at 10:47
  • \$\begingroup\$ Valid points. It's certainly a good trick for small classes. \$\endgroup\$ Commented Jul 9, 2015 at 10:49
  • 1
    \$\begingroup\$ @RobH Updated the answer to flag the performance aspect. \$\endgroup\$ Commented Jul 9, 2015 at 10:52
3
\$\begingroup\$

If you implement IComparable you probably want to also implement IComparable<T>, IEquatable<T>, ==, >, >=, < and <=. It's a bit of a pain but you'll find most things rely on each other:

public int CompareTo(object obj)
{
 if(obj == null) { return 1; }
 var updateId = obj as UpdateId;
 if (updateId == null)
 {
 throw new ArgumentException();
 }
 return CompareTo(updateId);
}
public int CompareTo(UpdateId updateId)
{
 // Shamelessly stolen from Heslacher
 int compareResult = this.Year.CompareTo(updateId.Year);
 if (compareResult != 0) { return compareResult; }
 compareResult = this.ReleaseNumber.CompareTo(updateId.ReleaseNumber);
 if (compareResult != 0) { return compareResult; }
 return this.Revision.CompareTo(updateId.Revision);
}
Heslacher
50.9k5 gold badges83 silver badges177 bronze badges
answered Jul 9, 2015 at 9:49
\$\endgroup\$
0

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.