C# Coding Guidelines (4) Types

C# Coding Guidelines:

In this part, type-related topics will be discussed, including design, usage, etc.

Value types vs. reference types

Consider designing a value type when

  • the type acts like a primitive type;
  • the type doesn't need to inherit from any other type;
  • the type will not have any other types derived from it;
  • instances of the type are not frequently passed as method arguments since this would cause frequent memory copy operations, hurting performance.

Actually, if there is no explicit need to design a struct, design a class by default.

In FCL, there are very few structs. System.Collections.Generic.KeyValuePair<TKey, TValue> is an example.

Atomicity

Prefer designing atomic types.

Design atomic types if possible, which makes code simple and brings fewer bugs.

This sample is from a legacy book “Effective C#”:

public class Address
{
    public string City { get; set; }

    public int ZipCode { get; set; }
}

Think about the above type. The first problem is, validation code is needed in each property setter. For example, zip code should not be negative.

The second problems is, the client code could be like this:

Address address = new Address();
address.City = "Bellevue";
address.ZipCode = 98007;

// ...

address.City = "Redmond";
address.ZipCode = 98053;

There are some invalid temporary status existing for the address object.

Another problem is, this design is obviously not thread-safe. If there are 10 threads reading a address instance, and 5 threads writing the address, it becomes complex.

This design is much better:

public class Address
{
    public Address(string city, int zipCode)
    {
        // Check arguments and validate.
        this.City = city;
        this.ZipCode = zipCode;
    }

    public string City { get; private set; }

    public int ZipCode { get; private set; }
}

The values can only be injected from the constructor, so the validation is centralized. Once the instance is constructed, its value cannot be changed. As a immutable or invariant type, it cannot have a invalid status, and it is also threading safe.

Type inferring

Use var for variables that:

  • you do not know its type, and
  • you do not need to know its type.

Actually var is mostly used because of anonymous type. Here is a sample:

var results = source.Where(item => item.Value > 20).Select(item => new
{
    Id = employee.Id,
    OrderCount = employee.Orders.Count()
});

foreach (var result in results)
{
}

The Select() query method returns a generic IEnumerable of some anonymous type generated by compiler.

Do not use var keyword in the other scenarios. In another way it means: do not use var if possible.

For example, these code are from a project:

var a = dictionary[key];
// ...
// ...
var b = GetSomething(a);
// ...
// ...
var c = b.Data;
// ...
// ...
var d = Process(c, x, y, z);
// ...
// ...
foreach (var e in d.Items) 
{
    // What the heck is e?
}

Continuously using var will make the code harder to read.

Dynamic types

Static typing where possible, dynamic typing when needed.

This is copied from the title of a paper, Static Typing Where Possible, Dynamic Typing When Needed: The End of the Cold War Between Programming Languages.

As Anders Hejlsberg said, When C# code is “talking to anything that isn’t statically typed to be a .NET class”, dynamic is a great solution. For example:

Type type = Type.GetTypeFromProgID("SAPI.SpVoice");
dynamic optimusPrime = Activator.CreateInstance(type);
optimusPrime.Speak("Autobots, transform, and roll out!");

The dynamic type saves a lot of time in interoperation.

Do not abuse dynamic when static typing is possible.

This rule need to be emphasized a lot. Otherwise these are going to happen:

  • performance hit;
  • no intellisense in IDE; 
  • a lot of errors cannot be checked at compile time.

Take the above Address class as an example:

dynamic address = new Address("Bellevue", 98007);
Console.WriteLine(address.City);
Console.WriteLine(address.State); // RuntimeBinderException

These code will be Ok to compile, but throw a RuntimeBinderException at runtime.

Here is another sample of abusage of dynamic:

internal class Program
{
    private static void Main()
    {
        dynamic number = 1;
        number += 1;
        string message = string.Format(CultureInfo.InvariantCulture, "The value of number is '{0}'.", number);
        Console.WriteLine(message);
    }
}

These code will be compiled into:

internal class Program
{
    [CompilerGenerated]
    private static class SiteContainer0
    {
        // Represents object = Add(object, 1).
        public static CallSite<Func<CallSite, object, int, object>> Add;

        // Represents object = string.Format(CultureInfo, string, object).
        public static CallSite<Func<CallSite, Type, CultureInfo, string, object, object>> Format;

        // Represents string = object.ToString().
        public static CallSite<Func<CallSite, object, string>> ToString;
    }

    private static void Main()
    {
        object number = 1;

        // Caches object = Add(object, 1).
        if (SiteContainer0.Add == null)
        {
            SiteContainer0.Add = CallSite<Func<CallSite, object, int, object>>.Create(
                Binder.BinaryOperation(
                    CSharpBinderFlags.None,
                    ExpressionType.TypeIs | ExpressionType.Lambda,
                    new CSharpArgumentInfo[] 
                { 
                    CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null), 
                    CSharpArgumentInfo.Create(
                        CSharpArgumentInfoFlags.LiteralConstant | CSharpArgumentInfoFlags.UseCompileTimeType, 
                        null) 
                }));
        }

        // Executes object = Add(object, 1).
        number = SiteContainer0.Add.Target.Invoke(SiteContainer0.Add, number, 1);

        // Caches object = string.Format(CultureInfo, string, object).
        if (SiteContainer0.Format == null)
        {
            SiteContainer0.Format = CallSite<Func<CallSite, Type, CultureInfo, string, object, object>>.Create(
                Binder.InvokeMember(
                    CSharpBinderFlags.None,
                    "Format",
                    null,
                    typeof(Program),
                    new CSharpArgumentInfo[] 
                { 
                    CSharpArgumentInfo.Create(
                        CSharpArgumentInfoFlags.IsStaticType | CSharpArgumentInfoFlags.UseCompileTimeType, 
                        null), 
                    CSharpArgumentInfo.Create(
                        CSharpArgumentInfoFlags.UseCompileTimeType, 
                        null), 
                    CSharpArgumentInfo.Create(
                        CSharpArgumentInfoFlags.LiteralConstant | CSharpArgumentInfoFlags.UseCompileTimeType, 
                        null), 
                    CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) 
                }));
        }

        // Executes object = string.Format(CultureInfo, string, object).
        object messageValue = SiteContainer0.Format.Target.Invoke(
            SiteContainer0.Format, 
            typeof(string), 
            CultureInfo.InvariantCulture, 
            "The value of number is '{0}'.", 
            number);

        // Caches string = object.ToString().
        if (SiteContainer0.ToString == null)
        {
            SiteContainer0.ToString = CallSite<Func<CallSite, object, string>>.Create(
                Binder.Convert(
                    CSharpBinderFlags.None,
                    typeof(string)));
        }

        // Executes string = object.ToString().
        string message = SiteContainer0.ToString.Target.Invoke(SiteContainer0.ToString, messageValue);

        Console.WriteLine(message);
    }        
}

It is obviously much better to write code with static typing, replacing dynamic with int:

internal class Program
{
    private static void Main()
    {
        int number = 1;
        number += 1;
        string message = string.Format(CultureInfo.InvariantCulture, "The value of number is '{0}'.", number);
        Console.WriteLine(message);
    }
}

2 Comments

Comments have been disabled for this content.