C# Corner

Create Anonymous Types

Taking a careful look at the capabilities the compiler gives to anonymous types provides an excellent tutorial on what you should consider when you create your types -- including whether they are classes or structs.

TECHNOLOGY TOOLBOX: C#

C# developers create types every day. But how often do developers think about how well their types behave with the type system? Do they write the methods and define the behavior their users will expect one day? What capabilities will the .NET Framework expect from any type?

Answering these questions isn't much work, but it's probably a bit more than you do today. Taking a look at the capabilities the compiler gives to anonymous types also serves as an excellent tutorial on what you should consider when you create your types.

Begin by creating a simple C# program that creates an anonymous type and prints each of the values in the sequence of those objects (see Go Online). The program isn't that interesting in itself, but it's instructive to look at the code the compiler generates for that anonymous type.

Now it's time to create a modified version of the code generated for this type (see Listing 1). The modified version changes many of the names because the compiler uses special characters for anonymous types to avoid collisions with user-created names. Also, the compiler creates a generic class, which it uses for some other optimizations.

Let's examine the important design decisions and the features added to the anonymous type. First, the accessibility of the class is internal. How often do you create internal classes? If your default choice is public, you should change that habit. How many classes do you create that will never be accessed outside of the current assembly? Are those public? If so, you're creating public classes unnecessarily, and you're adding names to your public API that aren't necessary. That creates more confusion for your users. They aren't supposed to use some of these classes, and yet, those classes show up in IntelliSense and in your API docs. This essentially pollutes your users' experience with your application. You should always choose the least-visible access to achieve your goal.

Anonymous types are sealed. This is an obvious choice because you can't derive a new type from something you can't name. Your job is somewhat more difficult. You need to examine your types and make an explicit decision to support or deny derived classes from your type. If you believe developers shouldn't extend your type, make that statement explicit by using the sealed keyword. If you believe developers will want to extend your type, pay careful consideration to where derived types will want to extend or change the behavior of your type. Make those methods virtual and provide explicit guidance on the extension points you've created. You should always support or prohibit derivation explicitly; don't leave it to client developer interpretation.

Check the Anonymous Type
Next, you should check whether the anonymous type is immutable. (VB.NET supports both mutable and immutable anonymous types.) Immutable types are simpler for many reasons: You can validate state in constructors and know they're always valid; immutable types can be shared safely across threads; and immutable types simplify testing because state changes can't affect later methods. However, it's overly simplistic to say that every type in your programs should be immutable; that's simply not practical. You should prefer immutable types whenever possible. When design issues force you to create mutable types, you should understand that those types are more complicated, will introduce more errors and will introduce more complications in testing. The more related data fields in a type, the more complicated the relationships are.

There are three methods added to every anonymous type by the C# compiler: overrides of Object.ToString(), Object.Equals() and Object.GetHashCode(). Object.ToString() helps in even this small sample. You can print the anonymous type object instead of writing extra code to print each of the fields from the anonymous type. Over the course of a larger application, a proper ToString() method makes it easier to use your type any time you want to display the information in an object for your users, or even in the debugger for other developers. You should always create a ToString() method that displays the best choice of information for your type.

The overrides of Object.Equals() and Object.GetHashCode() force anonymous types to use value semantics rather than reference semantics. That means two objects are equal if they contain the same information, rather than if they refer to the same object. If you modify Equals(), you do need to create a corresponding GetHashCode() to ensure that objects that are equal always return the same hash value. When you create your own types, you can choose to follow either reference semantics or value semantics. In general, types that are primarily data-storage types should follow value semantics. Types that are primarily defined by their behavior should follow reference semantics. Once again, make that an explicit decision: Determine which makes more sense for your type, and implement that appropriately.

Critique on Anonymous Types
Of course, nothing is perfect, including the implementation of anonymous types by the C# compiler. When you create your own types, you should consider additional decisions as part of every type you create. The first of these is related to equality semantics. The Object.Equals() override uses the ultimate base class, System.Object. As of .NET 2.0, you have a generic interface for equality: IEquatable<T>. Anytime you override System.Object.Equals(), you should implement IEquatable<T> for your type. For example, you should add IEquatable<T> support if your type is coded by hand:

internal sealed class HandCodedPoint : 
    IEquatable<HandCodedPoint>

The implementation of Equals() is a strongly typed version of the System.Object override. Notice that you can now defer the implementation of System.Object.Equals to your strongly typed version:

#region IEquatable<AnonymousTwo> Members
public bool Equals(HandCodedPoint other)
{
    return (((other != null) &&
     EqualityComparer<int>.Default.Equals(
     this.xField, other.xField)) &&
     EqualityComparer<int>.Default.Equals(
     this.yField, other.yField));
}
#endregion
public override bool Equals(object value)
{
    return Equals(value as HandCodedPoint);
}

If you've created your own version of Equals() and implemented IEquatable<T>, you should create your own operator == and operator !=. Because you've already written the logic in your IEquatable<T> method, you can reuse that implementation in your operator ==. Your operator != is the inverse:

public static bool operator ==(HandCodedPoint left, 
    HandCodedPoint right)
{
    if (left == null)
     return right == null;
    return left.Equals(right);
}
public static bool operator !=(
    HandCodedPoint left, 
    HandCodedPoint right)
{
    return !(left == right);
}

The lesson to take away from these examples: Equals touches many methods; make sure they're all consistent.

You should also consider implementing IComparable<T> (and the non-generic IComparable) if your type has an obvious ordering relation. This point type has an obvious ordering relation: using the distance of a point from the origin. You can compare those distances and define an ordering relation on points:

public int CompareTo(HandCodedPoint other)
{
    // Something is greater than nothing:
    if (other == null)
     return 1;
    int distanceSquared = 
     xField * xField + yField * yField;
    int otherDistanceSquared = 
     other.xField * other.xField + 
     other.yField + other.yField;
    return distanceSquared.CompareTo(
     otherDistanceSquared);

}
public int CompareTo(object obj)
{
    return CompareTo(
     obj as HandCodedPoint);
}

Implementing IComparable implies you have also defined operator > and operator <. You've already written the algorithm, so all you need to do is add the signature and call the methods you've already written:

public static bool operator >(
    HandCodedPoint 
    left, HandCodedPoint right)
{
    if (left == null)
     return false;
    else
     return left.CompareTo(right) > 0;
}
public static bool operator <(
    HandCodedPoint 
    left, HandCodedPoint right)
{
    if (left == null)
     return right != null;
    else
     return left.CompareTo(right) < 0;
}

HandCodedPoint implements both IComparable<T> and IEquatable<T>, so you should add the operators >= and <=:

public static bool operator >=(
    HandCodedPoint 
    left, HandCodedPoint right)
{
    return (left == right) || (left > right);
}
public static bool operator <=(HandCodedPoint 
    left, HandCodedPoint right)
{
    return (left == right) || (left < right);
}

This is a fair amount of work and I don't recommend adding it to every single type you create. However, you should give it some thought and make sure you know when you should and shouldn't care about certain behaviors. Obviously it doesn't make any sense to define an ordering relation between windows or many other types you create.

Anonymous types include a fair amount of code to implement the behavior that you would expect on every type. You should follow the same guidelines and create methods that all your users will expect or implement in their own version. You should also spend some time thinking of the basic behavior that's expected of your types, and create that behavior for your users. Rather than just ignoring the basic behavior, decide which behavior should be implemented in your type and create it.

About the Author

Bill Wagner, author of Effective C#, has been a commercial software developer for the past 20 years. He is a Microsoft Regional Director and a Visual C# MVP. His interests include the C# language, the .NET Framework and software design. Reach Bill at [email protected].

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube