5

Member Design

Methods, properties, events, constructors, and fields are collectively referred to as members. Members are ultimately the means by which framework functionality is exposed to the end users of a framework.

Members can be virtual or nonvirtual, concrete or abstract, static or instance, and can have several different scopes of accessibility. All this variety provides incredible expressiveness but at the same time requires care on the part of the framework designer.

This chapter offers basic guidelines that should be followed when designing members of any type. Chapter 6 spells out additional guidelines related to members that need to support extensibility.

5.1 General Member Design Guidelines

Most member design guidelines are specific to the kind of member being designed and are described later in this chapter. Nevertheless, some broad design conventions are applicable to different kinds of members. This section discusses these conventions.

5.1.1 Member Overloading

Member overloading means creating two or more members on the same type that differ only in the number or type of parameters but have the same name. For example, in the following, the WriteLine method is overloaded:

Click here to view code image

public static class Console {
  public void WriteLine();
  public void WriteLine(string value);
  public void WriteLine(bool value);
  ...
}

Because only methods, constructors, and indexed properties can have parameters, only those members can be overloaded.

Overloading is one of the most important techniques for improving the usability, productivity, and readability of reusable libraries. Overloading on the number of parameters makes it possible to provide simpler versions of constructors and methods. Overloading on the parameter type makes it possible to use the same member name for members performing identical operations on a selected set of different types.

For example, System.DateTime has several constructor overloads. The most powerful—but also the most complex—one takes eight parameters. Thanks to constructor overloading, the type also supports a shortened constructor that takes only three simple parameters: hours, minutes, and seconds.

Click here to view code image

public struct DateTime {
  public DateTime(int year, int month, int day,
            int hour, int minute, int second,
            int millisecond, Calendar calendar) { ... }

  public DateTime(int hour, int minute, int second) { ... }
}

This section does not cover the similarly named but quite different construct called operator overloading. Operator overloading is described in section 5.7.

Images DO try to use descriptive parameter names to indicate the default used by shorter overloads.

In a family of members overloaded on the number of parameters, the longer overload should use parameter names that indicate the default value used by the corresponding shorter member. This is mostly applicable to Boolean parameters. For example, in the following code, the first short overload does a case-sensitive look-up. The second, longer overload adds a Boolean parameter that controls whether the look-up is case sensitive. The parameter is named ignoreCase rather than caseSensitive to indicate that the longer overload should be used to ignore case and that the shorter overload probably defaults to the opposite, a case-sensitive look-up.

Click here to view code image

public class Type {
  public MethodInfo GetMethod(string name); //ignoreCase = false
  public MethodInfo GetMethod(string name, Boolean ignoreCase);
}

Images DO NOT arbitrarily vary parameter names in overloads. If a parameter in one overload represents the same input as a parameter in another overload, the parameters should have the same name.

Click here to view code image

public class String {
   // correct
   public int IndexOf (string value) { ... }
   public int IndexOf (string value, int startIndex) { ... }

   // incorrect
   public int IndexOf (string value) { ... }
   public int IndexOf (string str, int startIndex) { ... }
}

Images AVOID being inconsistent in the ordering of parameters in overloaded members. Parameters with the same name should appear in the same position in all overloads.

Click here to view code image

public class EventLog {
   public EventLog();
   public EventLog(string logName);
   public EventLog(string logName, string machineName);
   public EventLog(string logName, string machineName, string source);
}

In some specific cases, this otherwise very strict guideline can be broken. For example, a params array parameter has to be the last parameter in a parameter list. If a params array parameter appears in an overloaded member, the API designer might need to make a trade-off and either settle for inconsistent parameter ordering or not use the params modifier. For more information on the params array parameters, see section 5.8.4.

Another case where the guideline might need to be violated is when the parameter list contains out parameters. These parameters should typically appear at the end of the parameter list, which again means that the API designer might need to settle for a slightly inconsistent order of parameters in overloads with out parameters. See section 5.8 for more information on out parameters.

Images DO make only the longest overload virtual (if extensibility is required). Shorter overloads should simply call through to a longer overload.

Click here to view code image

public class Encoding {
   public bool IsAlwaysNormalized(){
     return IsAlwaysNormalized(NormalizationForm.FormC);
   }
   public virtual bool IsAlwaysNormalized(NormalizationForm form){
     //do real work here
   }
}

For more information on the design of virtual members, see Chapter 6.

Images DO NOT use ref, out, or in modifiers to overload members.

For example, you should not do the following:

Click here to view code image

public class SomeType {
  public void SomeMethod(string name){ ... }
  public void SomeMethod(out string name){ ... }
}

Some languages cannot resolve calls to overloads like this. In addition, such overloads usually have completely different semantics and probably should not be overloads at all, but rather two separate methods.

Images DO NOT have overloads with parameters at the same position and similar types, yet with different semantics.

The following example is a bit extreme, but it does illustrate the point:

Click here to view code image

public class SomeType {
  public void Print(long value, string terminator){
    Console.Write("{ʘ}{1}",value,terminator);
  }
  public void Print(int repetitions, string str){
    for(int i=ʘ; i< repetitions; i++){
        Console.Write(str);
    }
  }
}

The methods just presented should not be overloads. They should have two different names.

It’s difficult in some languages (in particular, dynamically typed languages) to resolve calls to these kinds of overloads. In most cases, if two overloads called with a number and a string would do exactly the same thing, it would not matter which overload is called. For example, the following is fine, because these methods are semantically equivalent:

Click here to view code image

public static class Console {
  public void WriteLine(long value){ ... }
  public void WriteLine(int value){ ... }
}

Images DO allow null to be passed for optional arguments.

If a method takes optional arguments that are reference types, allow null to be passed to indicate that the default value should be used. This avoids the problem of having to check for null before calling an API, as shown here:

Click here to view code image

if (geometry==null) DrawGeometry(brush, pen);
else DrawGeometry(brush, pen, geometry);

Images DO NOT have overloads with a generic type parameter in one method and a specific type in another.

When designing methods on a generic type, it may sometimes seem appropriate to have a generic-based overload and a System.String-based overload, as in this example:

Click here to view code image

public class PrettyPrinter<T> {
  public void PrettyPrint(string format){ ... }
  public void PrettyPrint(T otherPrinter){ ... }
  public void PrettyPrint(T otherPrinter, string format){ ... }
}

However, when the generic type is itself System.String, the overload that uses the generic type parameter becomes uncallable.

Click here to view code image

PrettyPrinter<string> printer = new PrettyPrinter<string>();
...
// C# overload resolution says this is PrettyPrint(string),
// not PrettyPrint(T).
// There's no way to call PrettyPrint(T) from C#.
printer.PrettyPrint("hello, world");

Images CONSIDER using default parameters on the longest overload of a method.

Default parameters are available in C#, F#, and VB.NET, but they are not CLS-compliant and may not be well supported by all languages that can use .NET libraries.

For the languages that do support default parameters, the use of default parameters can reduce the number of overloads that becomes possible for expressing every combination of optional parameters while still allowing the caller to use a somewhat minimal method invocation.

Click here to view code image

public static BigInteger Parse(
   ReadOnlySpan<char> value,
   NumberStyles style = NumberStyles.Integer,
   IFormatProvider provider = null) { ... }
...
BigInteger val = BigInteger.Parse(input, provider: formatCulture);

Images DO provide a simple overload, with no default parameters, for any method with two or more defaulted parameters.

Usability studies have shown that default parameters are a comparatively advanced feature. Many developers are confused with the presentation in IntelliSense, and sometimes think that they have to provide the defaulted arguments as exactly the value shown by IntelliSense. Providing the simple overload makes your method easier to approach for users who need only the default behaviors.

Click here to view code image

public static BigInteger Parse(ReadOnlySpan<char> value)
   // Since a provider is 'specified' this calls the overload
   // which uses default parameters.
   => Parse(value, provider: null);

public static BigInteger Parse(
   ReadOnlySpan<char> value,
   NumberStyles style = NumberStyles.Integer,
   IFormatProvider provider = null) { ... }

Images DO NOT use default parameters, for any parameter type other than CancellationToken, on interface methods or virtual methods on classes.

A disagreement between an interface declaration and the implementation on a default value creates an unnecessary source of confusion for your users. The same confusion also arises when a virtual method and an override of that method disagree on the default values for a parameter. The most straightforward solution to this problem is to avoid the situation, and only use default parameters on nonvirtual methods.

CancellationToken is specifically exempted from this guideline, because the guidance for asynchronous methods (section 9.2) is to always have a CancellationToken parameter with a default value. Since there’s only one legal default value for a CancellationToken— that is, default(CancellationToken)—no default value disagreement is possible between implementations.

Click here to view code image

public interface IExample {
 void PrintValue(int value = 5);
}

public class Example : IExample {
 public virtual void PrintValue(int value = 1ʘ) {
   Console.WriteLine(value);
 }
}
public class DerivedExample : Example {
 public override void PrintValue(int value = 2ʘ) {
   base.PrintValue(value);
 }
}

...

// What gets printed?
// If you have to think about it, the API is not self-documenting.
DerivedExample derived = new DerivedExample();
Example e = derived;
IExample ie = derived;
derived.PrintValue();
e.PrintValue();
ie.PrintValue();

For types that use the Template Method Pattern (section 9.9), the longest overload of the public nonvirtual method can use default parameters when appropriate, then defer to the virtual implementation method without using default parameters.

Click here to view code image

public partial class Control {
 // The public method uses a default parameter
 public void SetBounds(
   int x,
   int y,
   int width,
   int height,
   BoundsSpecified specified = BoundsSpecified.All) {
   ...
   SetBoundsCore(x, y, width, height, specified);
 }

 // The protected (virtual) method does not use default parameters
 protected virtual void SetBoundsCore(
   int x,
   int y,
   int width,
   int height,
   BoundsSpecified specified) {
   // Do the real work here.
 }
}

Default parameters can be provided for interface methods via extension methods.

Click here to view code image

public interface IExample {
 void PrintValue(int value);
}

public static class ExampleExtensions : IExample {
 public static void PrintValue(
   this IExample example,
   int value = 1ʘ) {

   // While this may look like a recursive call,
   // the C# method resolution rules say the instance member
   // from the interface is a better match than this extension
   // method.
   example.PrintValue(value);
 }

One proposed alternative to not using default parameters in virtual methods was to say that virtual overrides should always match the defaults from the original method declaration. However, that guideline leads to versioning problems when derived types exist in a different assembly:

  • Changing the last required parameter of a method to be a default parameter is generally not a breaking change, but would cause the override to provide a different set of defaults.

  • The guidance for adding a new default parameter to an existing method (provided next) calls for removing the defaults from the current method overload, which can result in ambiguous method compile-time failures due to the overrides that have yet to be updated.

Images DO NOT change the default value for a parameter once it has been publicly released.

Default parameters do not create logical overloads in the assembly that defines them, but are just a convenience mechanism to enable the compiler to fill in any missing values for the calling code at compile-time. When the value changes between two releases of a library, any existing callers will use the old value until they recompile, at which point they will switch to the new value. This can lead to confusion when diagnosing issues raised by users, when their reproduction cases don’t exhibit the bad behavior due to the changed default value.

If you want to be able to change the value over time, use an otherwise illegal sentinel value such as zero or -1 to indicate the runtime default value should be used instead.

Click here to view code image

// In this method, the style parameter should continue to use
// NumberStyles.Integer in all future versions of the library.
//
// Since the provider parameter has a default value of null,
// the library can change the default as an implementation detail
// (assuming that's an acceptable breaking change for the method).
public static BigInteger Parse(
   ReadOnlySpan<char> value,
   NumberStyles style = NumberStyles.Integer,
   IFormatProvider provider = null) { ... }

Images DO NOT have two overloads of a method with “compatible” required parameters that both use default parameters.

Images AVOID having two overloads of the same method that both use default parameters.

Images DO NOT have different defaults for the same parameter in two overloads of the same method.

The only time that two overloads of the same method should both have default parameters is when their required parameters have incompatible signatures. The parameters shared in common by the two overloads should have the same default values, or no default value. This makes it abundantly clear which overload is being called.

Click here to view code image

// Given this overload already exists
public static OperationStatus DecodeFromUtf8(
 ReadOnlySpan<byte> utf8,
 Span<byte> bytes,
 out int bytesConsumed,
 out int bytesWritten,
 bool isFinalBlock = true) { ... }

// OK: default parameters have the same value,
// and the signatures are incompatible
public static byte[] DecodeFromUtf8(
 byte[] utf8,
 out int bytesConsumed,
 bool isFinalBlock = true) { ... }

// BAD: default parameters have a different value
public static byte[] DecodeFromUtf8(
 byte[] utf8,
 out int bytesConsumed,
 bool isFinalBlock = false) { ... }

// BAD: This longer overload's required parameters are compatible
// with the original overload
public static OperationStatus DecodeFromUtf8(
 ReadOnlySpan<byte> utf8,
 Span<byte> bytes,
 out int bytesConsumed,
 out int bytesWritten,
 bool isFinalBlock = true,
 int bufferSize = 512) { ... }

Images DO move all default parameters to the new, longer overload when adding optional parameters to an existing method.

In the previous example, when adding the bufferSize parameter, the existing DecodeFromUtf8 method should change to no longer have a default value for the isFinalBlock parameter when the new overload is added. This method cannot be deleted without introducing a runtime breaking change (a MissingMethodException) in upgrade scenarios. Since having a simple parameter after an out parameter makes this DecodeFromUtf8 overload no longer conform to design guidelines, it is advisable to attribute it as [EditorBrowsable(EditorBrowsableState.Never)]. IntelliSense will only show the new overload with the additional optional parameters.

Click here to view code image

// This overload no longer has a default value for isFinalBlock,
// and has been marked as hidden from IntelliSense.
[EditorBrowsable(EditorBrowsableState.Never)]
public static OperationStatus DecodeFromUtf8(
 ReadOnlySpan<byte> utf8,
 Span<byte> bytes,
 out int bytesConsumed,
 out int bytesWritten,
 bool isFinalBlock) { /* call the longer overload */ }

public static OperationStatus DecodeFromUtf8(
 ReadOnlySpan<byte> utf8,
 Span<byte> bytes,
 out int bytesConsumed,
 out int bytesWritten,
 bool isFinalBlock = true,
 int bufferSize = 512) { ... }

5.1.2 Implementing Interface Members Explicitly

Explicit interface member implementation allows an interface member to be implemented so that it is only callable when the instance is cast to the interface type. For example, consider the following definition:

Click here to view code image

public struct Int32 : IConvertible {
  int IConvertible.ToInt32 () {..}
   ...
}

// calling ToInt32 defined on Int32
int i = ʘ;
i.ToInt32(); // does not compile
((IConvertible)i).ToInt32(); // works just fine

In general, implementing interface members explicitly is straightforward and follows the same general guidelines as those for methods, properties, or events. However, some specific guidelines apply to implementing interface members explicitly, as described next.

Images AVOID implementing interface members explicitly without having a strong reason to do so.

Explicitly implemented members can be confusing to developers because such members don’t appear in the list of public members and can also cause unnecessary boxing of value types.

Images CONSIDER implementing interface members explicitly if the members are intended to be called only through the interface.

This includes mainly members supporting framework infrastructure, such as data binding or serialization. For example, ICollection<T>. IsReadOnly is intended to be accessed mainly by the data-binding infrastructure through the ICollection<T> interface. It is almost never accessed directly when using types implementing the interface. Therefore, List<T> implements the member explicitly.

Images CONSIDER implementing interface members explicitly to simulate variance (change parameters or return types in “overridden” members).

For example, IList implementations often change the type of the parameters and returned values to create strongly typed collections by explicitly implementing (hiding) the loosely typed member and adding the publicly implemented strongly typed member.

Click here to view code image

public class StringCollection : IList {
  public string this[int index]{ ... }
  object IList.this[int index] { ... }
  ...
}

Images CONSIDER implementing interface members explicitly to hide a member and add an equivalent member with a better name.

You can say that this amounts to renaming a member. For example, System.Collections.Concurrent.ConcurrentQueue<T> implements IProducerConsumerCollection<T>.TryTake explicitly and renames it to TryDequeue.

Click here to view code image

partial class ConcurrentQueue<T> : IProducerConsumerCollection<T> {
  bool IProducerConsumerCollection<T>.TryTake(out T item) =>
    TryDequeue(out item);

  public bool TryDequeue(out T result) { ... }
}

Such member renaming should be done extremely sparingly. In most cases, the added confusion is a bigger problem than the suboptimal name of the interface member.

Images DO NOT use explicit members as a security boundary.

Such members can be called by any code by simply casting an instance to the interface.

Images DO provide a protected virtual member that offers the same functionality as the explicitly implemented member if the functionality is meant to be specialized by derived classes.

Explicitly implemented members cannot be overridden. They can be redefined, but then subtypes cannot call the base method’s implementation. It is recommended that you name the protected member by either using the same name or affixing Core to the interface member name.

Click here to view code image

[Serializable]
public class List<T> : ISerializable {
  ...
   void ISerializable.GetObjectData(
    SerializationInfo info, StreamingContext context) {
    GetObjectData(info,context);
   }

   protected virtual void GetObjectData(
    SerializationInfo info, StreamingContext context) {
    ...
   }
}

5.1.3 Choosing Between Properties and Methods

When designing members of a type, one of the most common decisions a library designer must make is to choose whether a member should be a property or a method.

On a high level, there are two general styles of API design in terms of usage of properties and methods. In method-heavy APIs, methods have a large number of parameters, and the types have fewer properties.

Click here to view code image

public class PersonFinder {
  public string FindPersonsName (
    int height,
    int weight,
    string hairColor,
    string eyeColor,
    int shoeSize,
    Connection database
  );
}

In property-heavy APIs, methods have a small number of parameters and more properties to control the semantics of the methods.

Click here to view code image

public class PersonFinder {
  public int Height { get; set; }
  public int Weight { get; set; }
  public string HairColor { get;set; }
  public string EyeColor { get; set; }
  public int ShoeSize { get; set; }

  public string FindPersonsName (Connection database);
}

All else being equal, the property-heavy design is generally preferable because methods with many parameters are less approachable to inexperienced developers. This is described in detail in Chapter 2.

Another reason to use properties when they are appropriate is that property values show up automatically in the debugger. By comparison, inspecting a value of a method is much more cumbersome.

However, it is worth noting that the method-heavy design has the advantage in terms of performance, and might result in better APIs for advanced users.

A rule of thumb is that methods should represent actions and properties should represent data. Properties are preferred over methods if everything else is equal.

Images CONSIDER using a property if the member represents a logical attribute of the type.

For example, Button.Color is a property because color is an attribute of a button.

Images DO use a property, rather than a method, if the value of the property is stored in the process memory and the property would just provide access to the value.

For example, a member that retrieves the name of a Customer from a field stored in the object should be a property.

Click here to view code image

public Customer {
  public Customer(string name){
    this.name = name;
  }
   public string Name {
    get { return this.name; }
  }
  private string name;
}

Images DO use a method, rather than a property, in the following situations:

  • The operation is orders of magnitude slower than a field access would be. If you are even considering providing an asynchronous version of an operation to avoid blocking the thread, it is very likely that the operation is too expensive to be a property. In particular, operations that access the network or the file system (other than once for initialization) should likely be methods, not properties.

  • The operation is a conversion, such as the Object.ToString method.

  • The operation returns a different result each time it is called, even if the parameters don’t change. For example, the Guid.NewGuid method returns a different value each time it is called.

  • The operation has a significant and observable side effect. Notice that populating an internal cache is not generally considered an observable side effect.

  • The operation returns a copy of an internal state (this does not include copies of value-type objects returned on the stack).

  • The operation returns an array.

Properties that return arrays can be very misleading. Usually it is necessary to return a copy of an internal array so that the user cannot change the internal state. This could lead to inefficient code.

In the following example, the Employees property is accessed twice in every iteration of the loop. That would be 2n + 1 copies for the following short code sample:

Click here to view code image

Company microsoft = GetCompanyData("MSFT");
for (int i = ʘ; i < microsoft.Employees.Length; i++) {
   if (microsoft.Employees[i].Alias == "kcwalina"){
       ...
   }
}

This problem can be addressed in one of two ways:

  • Change the property into a method, which communicates to callers that they are not just accessing an internal field and probably are creating an array every time they call the method. Given that, users are more likely to call the method once, cache the result, and work with the cached array.

    Click here to view code image

    Company microsoft = GetCompanyData("MSFT");
    Employees[] employees = microsoft.GetEmployees();
    for (int i = ʘ; i < employees.Length; i++) {
       if (employees[i].Alias == "kcwalina"){
           ...
       }
    }
  • Change the property to return a collection instead of an array. You can use ReadOnlyCollection<T> to provide public read-only access to a private array. Alternatively, you can use a subclass of Collection<T> to provide controlled read-write access, so that you can be notified when the user code modifies the collection. See section 8.3 for more details on using ReadOnlyCollection<T> and Collection<T>.

    Click here to view code image

    public ReadOnlyCollection<Employee> Employees {
       get { return roEmployees; }
    }
    private Employee[] employees;
    private ReadOnlyCollection<Employee> roEmployees;

5.2 Property Design

Although properties are technically very similar to methods, they are quite different in terms of their usage scenarios. They should be seen as smart fields. They have the calling syntax of fields, and the flexibility of methods.

Images DO create get-only properties if the caller should not be able to change the value of the property.

If the type of the property is a mutable reference type, the property value can be changed even if the property is get-only.

Images DO NOT provide set-only properties or properties with the setter having broader accessibility than the getter.

For example, do not use properties with a public setter and a protected getter.

If the property getter cannot be provided, implement the functionality as a method instead. Consider starting the method name with Set and follow with what you would have named the property. For example, AppDomain has a method called SetCachePath instead of having a set-only property called CachePath.

Images DO provide sensible default values for all properties, ensuring that the defaults do not result in a security hole or terribly inefficient code.

Images DO allow properties to be set in any order, even if this results in a temporary invalid state of the object.

It is common for two or more properties to be interrelated to a point where some values of one property might be invalid given the values of other properties on the same object. In such cases, exceptions resulting from the invalid state should be postponed until the interrelated properties are actually used together by the object.

Images DO preserve the previous value if a property setter throws an exception.

Images AVOID throwing exceptions from property getters.

Property getters should be simple operations and should not have any preconditions. If a getter can throw an exception, it should probably be redesigned to be a method. Notice that this rule does not apply to indexers, where we do expect exceptions as a result of validating the arguments.

5.2.1 Indexed Property Design

An indexed property is a special property that can have parameters and can be called with special syntax similar to array indexing.

Click here to view code image

public class String {
  public char this[int index] {
    get { ... }
  }
}
...

string city = "Seattle";
Console.WriteLine(city[0]); // this will print 'S'

Indexed properties are commonly referred to as indexers. Indexers should be used only in APIs that provide access to items in a logical collection. For example, a string is a collection of characters, and the indexer on System.String exists to access its characters.

Images CONSIDER using indexers to provide access to data stored in an internal array.

Images CONSIDER providing indexers on types representing collections of items.

Images AVOID using indexed properties with more than one parameter.

If the design requires multiple parameters, reconsider whether the property really represents an accessor to a logical collection. If it does not, use methods instead. Consider starting the method name with Get or Set.

Images AVOID indexers with parameter types other than System.Int32, System.Int64, System.String, System.Range, System.Index, or an enum, except on dictionary-like types.

If the design requires other types of parameters, strongly reevaluate whether the API really represents an accessor to a logical collection. If it does not, use a method. Consider starting the method name with Get or Set.

Images DO use the name Item for indexed properties unless there is an obviously better name (e.g., see the Chars property on System.String).

In C#, indexers are by default named Item. The IndexerNameAttribute can be used to customize this name.

Click here to view code image

public sealed class String {
 [System.Runtime.CompilerServices.IndexerNameAttribute("Chars")]
 public char this[int index] {
   get { ... }
 }
 ...
}

Images DO NOT provide both an indexer and methods that are semantically equivalent.

In the following example, the indexer should be changed to a method.

Click here to view code image

// Bad design
public class Type {
  [System.Runtime.CompilerServices.IndexerNameAttribute("Members")]
  public MemberInfo this[string memberName]{ ... }
  public MemberInfo GetMember(string memberName, Boolean ignoreCase){
... }
}

Images DO NOT provide more than one family of overloaded indexers in one type.

This is enforced by the C# compiler.

Images DO NOT use nondefault indexed properties.

This is enforced by the C# compiler.

Images DO return the declaring type from an indexer that accepts System.Range.

Range-based indexers on a collection should return a collection of the same type as the type that declares them. This includes implicit indexers created by having a Slice method with the appropriate signature. Instead of violating this guideline when your goal is to create a different type, provide a separate conversion method. For example, arrays produce a copy of the requested range in the Range indexer, but arrays also have the array.AsSpan(Range) extension method to produce a Span<T> for the same range of indices.

Click here to view code image

// OK: Returns the declaring type from a Range-based indexer
public partial class SomeCollection {
  public SomeCollection this[Range range] { get { ... } }
}

// OK: Returns the declaring type from Slice(int, int), which
// makes an implicit indexer in C#
public partial class SomeCollection {
  public SomeCollection Slice(int offset, int length) { ... }
}

// BAD: Returns the wrong type from a Range-based indexer
public partial class SomeCollection {
  public SomeValue[] this[Range range] { get { ... } }
}

// BAD: Returns the wrong type from Slice(int, int), which
// makes an implicit indexer in C#
public partial class SomeCollection {
  public SomeValue[] Slice(int offset, int length) { ... }
}

5.2.2 Property Change Notification Events

Sometimes it is useful to provide an event notifying the user of changes in a property value. For example, System.Windows.Forms.Control raises a TextChanged event after the value of its Text property has changed.

Click here to view code image

public class Control : Component{
  string text = String.Empty;

  public event EventHandler<EventArgs> TextChanged;
  public string Text{
     get{ return text; }
     set{
       if (text!=value) {
           text = value;
           OnTextChanged();
       }
     }
 }

 protected virtual void OnTextChanged(){
    EventHandler<EventArgs> handler = TextChanged;
    if(handler!=null){
      handler(this,EventArgs.Empty);
    }
  }
}

The guidelines that follow describe when property change events are appropriate and their recommended design for these APIs.

Images CONSIDER raising change notification events when property values in high-level APIs (usually designer components) are modified.

If there is a good scenario for a user to know when a property of an object is changing, the object should raise a change notification event for the property.

However, it is unlikely to be worth the overhead to raise such events for low-level APIs such as base types or collections. For example, List<T> would not raise such events when a new item is added to the list and the Count property changes.

Images CONSIDER raising change notification events when the value of a property changes via external forces.

If a property value changes via some external force (in a way other than by calling methods on the object), raising events indicates to the developer that the value is changing or has changed. A good example is the Text property of a text box control. When the user types text in a TextBox, the property value automatically changes.

5.3 Constructor Design

There are two kinds of constructors: type constructors and instance constructors.

Click here to view code image

public class Customer {
  public Customer() { ... } // instance constructor
  static Customer() { ... } // type constructor
}

Type constructors are static and are run by the CLR before the type is used. Instance constructors run when an instance of a type is created.

Type constructors cannot take any parameters; instance constructors can. Instance constructors that don’t take any parameters are often called default constructors.

Constructors are the most natural way to create instances of a type. Most developers will search for, and try to use, a constructor before they consider alternative ways of creating instances (such as factory methods).

Images CONSIDER providing simple, ideally default, constructors.

A simple constructor has a very small number of parameters, and all parameters are primitives or enums. Such simple constructors increase the usability of the framework.

Images CONSIDER using a static factory method instead of a constructor if the semantics of the desired operation do not map directly to the construction of a new instance, or if following the constructor design guidelines feels unnatural.

See section 9.5 for more details on factory method design.

Images DO use constructor parameters as shortcuts for setting main properties.

There should be no difference in semantics between using the empty constructor followed by some property sets and using a constructor with multiple arguments. The following three code examples are equivalent:

Click here to view code image

//1
var applicationLog = new EventLog();
applicationLog.MachineName = "BillingServer";
applicationLog.Log = "Application";

//2
var applicationLog = new EventLog("Application");
applicationLog.MachineName = "BillingServer";

//3
var applicationLog = new EventLog("Application", "BillingServer");

Images DO use the same name for constructor parameters and a property if the constructor parameters are used just to set the property.

The only difference between such parameters and the properties should be casing.

Click here to view code image

public class EventLog {
  public EventLog(string logName){
    this.LogName = logName;
  }
  public string LogName {
    get { ... }
    set { ... }
  }
}

Images CONSIDER adding a property for each parameter in a constructor.

Developers generally find it helpful when they can inspect the state of an object for debugging or logging purposes. While a debugger can usually inspect the private fields of an object, that isn’t easy to do with logging. Exposing at least a get-only property for each constructor parameter enables users to incorporate the state of your object into their diagnostic flow.

Constructor parameters typed as mutable reference types may not be suitable for exposing as properties, particularly when your type is stateful and modifying that parameter can invalidate your state. You should also be mindful of the guidance for when to use a method instead of a property (see section 5.1.3).

You may have other reasons, such as data hiding, for not exposing constructor parameters via properties. Even so, you should generally start from an expectation of adding the properties and then justify why doing so is not appropriate in context.

Images DO minimal work in the constructor.

Constructors should not do much work other than capture the constructor parameters. The cost of any other processing should be delayed until required.

Images DO throw exceptions from instance constructors, if appropriate.

Images DO explicitly declare the public default constructor in classes, if such a constructor is required.

If you don’t explicitly declare any constructors on a type, many languages (such as C#) will automatically add a public default constructor. (Abstract classes get a protected constructor.) For example, the following two declarations are equivalent in C#:

public class Customer {
}

public class Customer {
  public Customer(){}
}

Adding a parameterized constructor to a class prevents the compiler from adding the default constructor. This often causes accidental breaking changes. Consider a class defined as shown in the following example:

public class Customer {
}

Users of the class can call the default constructor, which the compiler automatically added in this case, to create an instance of the class.

Click here to view code image

var customer = new Customer();

It is quite common to add a parameterized constructor to an existing type with a default constructor. If the addition is not done carefully, however, the default constructor might no longer be emitted. For example, the following addition to the type just declared will “remove” the default constructor:

Click here to view code image

public class Customer {
  public Customer(string name) { ... }
}

This will break code relying on the default constructor, and such a problem is unlikely to be caught in a code review. Therefore, the best practice is to always specify the public default constructor explicitly.

Note that this does not apply to structs. Structs implicitly get default constructors even if they have a parameterized constructor defined.

Images AVOID explicitly defining default constructors on structs.

Many CLR languages do not allow developers to define default constructors on value types.1 Users of these languages are often surprised to learn that default(SomeStruct) and new SomeStruct() don’t necessarily produce the same value. Even if your language does allow defining a default constructor on a value type, it probably isn’t worth the confusion it would cause if you did so.

1. A struct without an explicitly defined constructor still gets a default constructor that is provided by the CLR implicitly; it assigns all fields to their “zero” value (0, false, null).

Click here to view code image

public struct Token {
  public Token(Guid id) { this.id = id; }
  internal Guid id;
}
...
var token = new Token(); // this compiles and executes just fine

The runtime will initialize all of the fields of the struct to their default values (0/null).

Images AVOID calling virtual members on an object inside its constructor.

Calling a virtual member will cause the most derived override to be called, even if the constructor of the most derived type has not been fully run yet.

Consider the following example, which prints out “What is wrong?” when a new instance of Derived is created. The implementer of the derived class assumes that the value will be set before anyone can call Method. However, that is not true: The Base constructor is called before the Derived constructor finishes, so any calls it makes to Method might operate on data that is not yet initialized.

Click here to view code image

public abstract class Base {
  public Base() {
     Method();
  }
  public abstract void Method();
}

public class Derived: Base {
  private int value;
  public Derived() {
    value = 1;
  }

  public override void Method() {
    if (value == 1){
       Console.WriteLine("All is good");
    }
    else {
        Console.WriteLine("What is wrong?");
    }
  }
}

Occasionally, the benefits associated with calling virtual members from a constructor might outweigh the risks. An example is a helper constructor that initializes virtual properties using parameters passed to the constructor. It’s acceptable to call virtual members from constructors, given that all the risks are carefully analyzed and you document the virtual members that you call for the users overriding the virtual members.

5.3.1 Type Constructor Guidelines

A type constructor, also called a static constructor, is used to initialize a type. The runtime calls the static constructor before the first instance of the type is created or any static members of the type are accessed.

Images DO make static constructors private.

A static constructor, also called a class constructor, is used to initialize a type. The CLR calls the static constructor before the first instance of the type is created or any static members of that type are called. The user has no control over when the static constructor is called. If a static constructor is not private, it can be called by code other than the CLR. Depending on the operations performed in the constructor, this can cause unexpected behavior.

The C# compiler forces static constructors to be private.

Images DO NOT throw exceptions from static constructors.

If an exception is thrown from a type constructor, the type is not usable in the current application domain.

Images CONSIDER initializing static fields inline rather than explicitly using static constructors, because the runtime is able to optimize the performance of types that don’t have an explicitly defined static constructor.

Click here to view code image

// unoptimized code
public class Foo {
  public static readonly int Value;
  static Foo() {
    Value = 63;
  }
  public static void PrintValue() {
    Console.WriteLine(Value);
  }
}

// optimized code
public class Foo {
  public static readonly int Value = 63;
  public static void PrintValue() {
     Console.WriteLine(Value);
  }
}

5.4 Event Design

Events are the most commonly used form of callbacks (constructs that allow the framework to call into user code). Other callback mechanisms include members taking delegates, virtual members, and interface-based plug-ins. Data from usability studies indicates that the majority of developers are more comfortable using events than they are using the other callback mechanisms. Events are nicely integrated with Visual Studio and many languages.

Under the covers, events are not much more than fields that have a type that is a delegate, plus two methods to manipulate the field. Delegates used by events have special signatures (by convention) and are referred to as event handlers.

When users subscribe to an event, they provide an instance of the event handler bound to a method that will be called when the event is raised. The method provided by the user is referred to as an event handling method.

The event handler determines the event handling method’s signature. By convention, the return type of the method is void and the method takes two parameters. The first parameter represents the object that raised the event. The second parameter represents event-related data that the object raising the event wants to pass to the event handling method. This data is often referred to as event arguments.

Click here to view code image

var timer = new Timer(1ʘʘʘ);
timer.Elapsed += new ElapsedEventHandler(TimerElapsedHandlingMethod);
...
// event handling method for Timer.Elapsed
void TimerElapsedHandlingMethod(object sender, ElapsedEventArgs e){
    ...
}

There are two groups of events: events raised before a state of the system changes, called pre-events, and events raised after a state changes, called post-events. An example of a pre-event would be Form.Closing, which is raised before a form is closed. An example of a post-event would be Form.Closed, which is raised after a form is closed. The following example shows an AlarmClock class defining an AlarmRaised post-event.

Click here to view code image

public class AlarmClock {
  public AlarmClock() {
     timer.Elapsed += new ElapsedEventHandler(TimerElapsed);
  }

  public event EventHandler<AlarmRaisedEventArgs> AlarmRaised;

  public DateTimeOffset AlarmTime {
    get { return alarmTime; }
    set {
      if (alarmTime != value) {
         timer.Enabled = false;
         alarmTime = value;
         TimeSpan delay = alarmTime - DateTimeOffset.Now;
         timer.Interval = delay.TotalMilliseconds;
         timer.Enabled = true;
      }
    }
 }

  protected virtual void OnAlarmRaised(AlarmRaisedEventArgs e){
    EventHandler<AlarmRaisedEventArgs> handler = AlarmRaised;
    if (handler != null) {
       handler(this, e);
    }
  }

  private void TimerElapsed(object sender, ElapsedEventArgs e){
    OnAlarmRaised(AlarmRaisedEventArgs.Empty);
  }

  private Timer timer = new Timer();
  private DateTimeOffset alarmTime;
}
public class AlarmRaisedEventArgs : EventArgs {
  new internal static readonly
     AlarmRaisedEventArgs Empty = new AlarmRaisedEventArgs();
}

Images DO use the term “raise” for events rather than “fire” or “trigger.”

When referring to events in documentation, use the phrase “an event was raised” instead of “an event was fired” or “an event was triggered.”

Images DO use System.EventHandler<T> instead of manually creating new delegates to be used as event handlers.

Click here to view code image

public class NotifyingContactCollection : Collection<Contact> {
  public event EventHandler<ContactAddedEventArgs> ContactAdded;
   ...
}

If you’re adding new events to an existing feature area that uses traditional event handlers, then continue using the existing event handler types to remain consistent within the feature area, and use new custom event handler types if using the generic handler feels inconsistent with the feature area. For example, when defining new events related to types in the System.Windows.Forms namespace, you might want to continue using manually created handlers.

Creating new custom event handlers is rare, and the guidance has been archived to Appendix B.

Images CONSIDER using a subclass of EventArgs as the event argument, unless you are absolutely sure the event will never need to carry any data to the event handling method, in which case you can use the EventArgs type directly.

Click here to view code image

public class AlarmRaisedEventArgs : EventArgs {
}

If you ship an API using EventArgs directly, you will never be able to add any data to be carried with the event without breaking compatibility. If you use a subclass, even if it is initially completely empty, you will be able to add properties to the subclass when needed.

Click here to view code image

public class AlarmRaisedEventArgs : EventArgs {
   public DateTimeOffset AlarmTime { get; }
}

Images DO use a protected virtual method to raise each event. This is applicable only to nonstatic events on unsealed classes—not to structs, sealed classes, or static events.

For each event, include a corresponding protected virtual method that raises the event. The purpose of the method is to provide a way for a derived class to handle the event using an override. Overriding is a more flexible, faster, and more natural way to handle base class events in derived classes. By convention, the name of the method should start with “On” and be followed with the name of the event.

The derived class can choose not to call the base implementation of the method in its override. Be prepared for this by not including any processing in the method that is required for the base class to work correctly.

Images DO take one parameter to the protected method that raises an event.

The parameter should be named e and should be typed as the event argument class.

Click here to view code image

protected virtual void OnAlarmRaised(AlarmRaisedEventArgs e){
  AlarmRaised?.Invoke(this, e);
}

Images DO NOT pass null as the sender when raising a nonstatic event.

Images DO pass null as the sender when raising a static event.

Click here to view code image

EventHandler<EventArgs> handler = ...;
if (handler!=null) handler(null,...);

Images DO NOT pass null as the event data parameter when raising an event.

You should pass EventArgs.Empty if you don’t want to pass any data to the event handling method. Developers expect this parameter not to be null.

Images CONSIDER raising events that the end user can cancel. This guideline applies only to pre-events.

Use System.ComponentModel.CancelEventArgs or its subclass as the event argument to allow the end user to cancel events. For example, System.Windows.Forms.Form raises a Closing event before a form closes. The user can cancel the close operation, as shown in the following example:

Click here to view code image

void ClosingHandler(object sender, CancelEventArgs e) {
  e.Cancel = true;
}

5.5 Field Design

The principle of encapsulation is one of the most important notions in object-oriented design. This principle states that data stored inside an object should be accessible only to that object.

A useful way to interpret the principle is to say that a type should be designed so that changes to fields of that type (name or type changes) can be made without breaking code other than for members of the type. This interpretation immediately implies that all fields must be private.

We exclude constant and static read-only fields from this strict restriction, because such fields, almost by definition, are never required to change.

Images DO NOT provide instance fields that are public or protected.

You should provide properties for accessing fields instead of making them public or protected.

Very trivial property accessors, as shown here, can be inlined by the Just-in-Time (JIT) compiler and provide performance on par with that of accessing a field.

public struct Point{
   private int x;
   private int y;

   public Point(int x, int y){
      this.x = x;
      this.y = y;
   }

   public int X {
     get{ return x; }
  }

   public int Y{
     get{ return y; }
   }
}

By not exposing fields directly to the developer, the type can be versioned more easily, and for the following reasons:

  • A field cannot be changed to a property while maintaining binary compatibility.

  • The presence of executable code in get and set property accessors allows later improvements, such as on-demand creation of an object for usage of the property, or a property change notification.

Images DO use constant fields for constants that will never change.

The compiler burns the values of const fields directly into calling code. Therefore, const values can never be changed without the risk of breaking compatibility.

Click here to view code image

public struct Int32 {
   public const int MaxValue = ʘx7fffffff;
   public const int MinValue = unchecked((int)ʘx8ʘʘʘʘʘʘʘ);
}

Images CONSIDER using public static readonly fields for predefined object instances.

If there are predefined instances of the type, declare them either as public static readonly fields or as get-only properties on the type itself.

Click here to view code image

public struct Color{
  public static readonly Color Red = new Color(ʘxʘʘʘʘFF);
  public static readonly Color Green = new Color(ʘxʘʘFFʘʘ);
  public static readonly Color Blue = new Color(ʘxFFʘʘʘʘ);
  ...
}

When a type has a significant number of predefined instances, but few are used in any application, you may prefer to use get-only properties. Because the .NET Runtime will initialize all of the static fields at the same time, a type with many predefined instances can suffer from visible start-up performance. When exposed as properties, the predefined values can be built on demand, like the System.Text.Encoding.UTF8 value:

Click here to view code image

public static Encoding UTF8 {
   get {
      return _utf8Encoding ??= new UTF8Encoding();
   }
}

Images DO NOT assign instances of mutable types to public or protected readonly fields.

A mutable type is a type with instances that can be modified after they are instantiated. For example, arrays, most collections, and streams are mutable types, but System.Int32, System.Uri, and System.String are all immutable. The readonly modifier on a reference type field prevents the instance stored in the field from being replaced, but it does not prevent the field’s instance data from being modified by calling members changing the instance. The following example shows how it is possible to change the value of an object referred to by a readonly field.

Click here to view code image

public class SomeType {
  public static readonly int[] Numbers = new int[1ʘ];
}
...
SomeType.Numbers[5] = 1ʘ; // changes a value in the array

5.6 Extension Methods

Extension methods are a language feature that allows static methods to be called using instance method call syntax. These methods must take at least one parameter, which represents the instance the method is to operate on. For example, in C#, you declare extension methods by placing the this modifier on the first parameter.

Click here to view code image

public static class StringExtensions {
  public static bool IsPalindrome(this string s){
    ...
  }
}

This extension method can be called as follows:

Click here to view code image

if("hello world".IsPalindrome()){
     ...
}

The class that defines such extension methods must be declared as a static class. To use extension methods, you must import the namespace defining the class that contains the extension method.

Images AVOID frivolously defining extension methods, especially on types outside your library or framework.

If you do own source code of a type, consider using regular instance methods instead. If you don’t own the type, but you want to add a method, be very careful. Liberal use of extension methods has the potential to clutter APIs with types that were not designed to have these methods.

Another thing to consider is that extension methods are a compile-time facility, and not all languages provide support for them. Callers using languages without extension method support will have to use the regular static method call syntax to call your extension methods.

There are, of course, scenarios in which extension methods should be employed. These are outlined in the guidelines that follow.

Images CONSIDER using extension methods to provide helper functionality relevant to every implementation of an interface.

The primary example of a feature using this guidance is the System.Linq namespace. The methods defined on System.Linq.Enumerable— such as Select, OrderBy, First, and Last—are both capable of operating on any IEnumerable<T> implementation and relevant to any IEnumerable<T> instance.

Many of the methods on the Enumerable class do runtime type checks to use more efficient implementations. For example, IList<T> has a positional indexer and a Count property, so the Last method can call that property to execute much faster than doing a foreach over everything in the IEnumerable<T>. This highlights the main drawback of using extension methods for interface functionality—the lack of polymorphism. If polymorphism is required for the helper functionality to be practical, then providing functionality via an extension method will just set your users up for a bad experience.

Images CONSIDER using extension methods when an instance method would introduce an unwanted dependency.

For example, when the ReadOnlySpan<T> type was originally introduced, it was in a stand-alone library. Rather than waiting for an opportunity for String to depend on ReadOnlySpan<char> for the AsSpan() method and having a strong dependency between these types, the AsSpan method for String was written as an extension method in the library that defined ReadOnlySpan<T>.

Images CONSIDER using generic extension methods when an instance method on a generic type is not well defined for all possible type parameters.

For example, a collection of integers has a well-defined behavior for a method like GetMinimumValue, but the method is less well defined when the collection contains HttpContext values. By using a generic extension method, the GetMinimumValue method can specify more restrictive type constraints than the collection generic type.

Click here to view code image

// The collection type has no type restrictions
public class SomeCollection<T> { ... }

// A non-generic extensions class
public sealed class SomeCollectionComparisons {
   // This method is only defined when the collection is
   // of comparable values
   public static T GetMinimumValue<T>(
      this SomeCollection<T> source) where T : IComparable<T> {
      ...
   }
}

In addition to making use of generic constraints to restrict applicable types, some methods only make sense on specific generic instantiations, such as the IsWhiteSpace method for ReadOnlySpan<char>.

Click here to view code image

public static bool IsWhiteSpace(this ReadOnlySpan<char> span) {...}

Images DO throw an ArgumentNullException when the this parameter in an extension method is null.

Extension methods are just static methods with some extra “syntactic sugar” in C# and some other languages, and they should perform argument validation like any other method. The guidance for NullReferenceException from section 7.3.5 says it should never be thrown from a publicly callable method, and that also applies to extension methods even when invoked in “instance” syntax.

Silently operating on null inputs, and throwing no exception at all, generally confuses developers debugging “impossible” states in their program—when the actually failing code is “clearly” unreachable, because if this was a null value the previous statement would have thrown instead. Even if the method would be well behaved with a null input as a simple static method, once the this modifier is added the method should throw an exception for a null first argument.

Click here to view code image

// This should throw an ArgumentNullException
SomeExtensions.SomeMethod(null);

// This is the same at runtime,
// so it should throw the same kind of exception.
((SomeType)null).SomeMethod();

Images AVOID defining extension methods on System.Object.

VB users will not be able to call such methods on object references using the extension method syntax, because VB does not support calling such methods. In VB, declaring a reference as Object forces all method invocations on it to be late bound (the actual member called is determined at runtime), while bindings to extension methods are determined at compile-time (early bound). For example:

Click here to view code image

// C# declaration of the extension method
public static class SomeExtensions{
  static void SomeMethod(this object o){...}
}

' VB will fail to compile as VB.NET does not support calling extension
' methods on reference types as Object
Dim o As Object = ...
o.SomeMethod();

VB users will have to call the method using the regular static method call syntax.

SomeExtensions.SomeMethod(o)

Note that this guideline applies to other languages where the same binding behavior is present, or where extension methods are not supported.

Images CONSIDER naming the type that holds extension methods for its functionality—for example, use “Routing” instead of “[ExtendedType] Extensions.”

In other words, design the static class that holds the extension methods as you would any other static class; then make some of the methods be extension methods, when appropriate.

This provides for a much better experience in languages without extension syntax, and allows the static class to be the correct choice for where a related method should go even when it can’t be used in extension syntax.

Click here to view code image

// Bad: This class has too many purposes
public static class SomeCollectionExtensions {

 public static T GetMinimumValue<T>(
   this SomeCollection<T> source) where T : IComparable<T> { ... }

 public static T GetMaximumValue<T>(
   this SomeCollection<T> source) where T : IComparable<T> { ... }

 public static SomeCollection<TNested> Flatten(
   this SomeCollection<TOuter> source)
   where TOuter : IEnumerable<TNested> { ... }
}

// Good: These classes each have one purpose
public static class SomeCollectionComparisons {
 public static T GetMinimumValue<T>(
   this SomeCollection<T> source) where T : IComparable<T> { ... }

 public static T GetMaximumValue<T>(
   this SomeCollection<T> source) where T : IComparable<T> { ... }
}

public static class SomeCollectionNestedOperations {
 public static SomeCollection<TNested> Flatten(
   this SomeCollection<TOuter> source)
   where TOuter : IEnumerable<TNested> { ... }
}

Images DO NOT put extension methods in the same namespace as the extended type unless the intention is to add methods to interfaces, for generic type restriction, or for dependency management.

Of course, in the case of dependency management, the type with the extension methods would be in a different assembly.

Images AVOID defining two or more extension methods with the same signature, even if they reside in different namespaces.

For example, if two different namespaces defined the same extension method on the same type, it would be impossible to import both namespaces in the same file. The compiler would report an ambiguity if one of the methods was called.

Click here to view code image

namespace A {
  public static class AExtensions {
    public static void ExtensionMethod(this Foo foo){...}
  }
}
namespace B {
  public static class BExtensions {
    public static void ExtensionMethod(this Foo foo){...}
  }
}

Images CONSIDER defining extension methods in the same namespace as the extended type if the type is an interface and if the extension methods are meant to be used in most or all cases.

Images CONSIDER defining extension methods in the same namespace as the extended type if the type is generic and if the extension methods apply stronger type parameter restrictions than the extended type does.

The following example uses an extension method to provide a Trim method for ReadOnlyMemory<char> without defining it for all ReadOnlyMemory<T> types.

Click here to view code image

public partial struct ReadOnlyMemory<T> { ... }

public static partial class MemoryExtensions {
   public static ReadOnlyMemory<char> Trim(
      this ReadOnlyMemory<char> memory) { ... }
}

This technique can also be used with a generic method with generic restrictions.

Click here to view code image

public static partial class CollectionDisposer {
   public static void DisposeAll<T>(
      this List<T> list) where T : IDisposable { ... }
}

Images DO NOT define extension methods implementing a feature in namespaces normally associated with other features. Instead, define them in the namespace associated with the feature they belong to.

Similar to the guidance that the name of the class containing the extension method definitions should reflect its functionality, choose the namespace of the defining class to match the functionality instead of based on the fact that it is providing extension methods.

Remember that not all languages can use extension invocation, and consider the static method invocation experience.

5.7 Operator Overloads

Operator overloads allow framework types to appear as if they were built-in language primitives. The following snippet shows some of the most important operator overloads defined by System.Decimal.

Click here to view code image

public struct Decimal {
  public static Decimal operator+(Decimal d);
  public static Decimal operator-(Decimal d);
  public static Decimal operator++(Decimal d);
  public static Decimal operator--(Decimal d);
  public static Decimal operator+(Decimal d1, Decimal d2);
  public static Decimal operator-(Decimal d1, Decimal d2);
  public static Decimal operator*(Decimal d1, Decimal d2);
  public static Decimal operator/(Decimal d1, Decimal d2);
  public static Decimal operator%(Decimal d1, Decimal d2);
  public static bool operator==(Decimal d1, Decimal d2);
  public static bool operator!=(Decimal d1, Decimal d2);
  public static bool operator<(Decimal d1, Decimal d2);
  public static bool operator<=(Decimal d1, Decimal d2);
  public static bool operator>(Decimal d1, Decimal d2);
  public static bool operator>=(Decimal d1, Decimal d2);

  public static implicit operator Decimal(int value);
  public static implicit operator Decimal(long value);
  public static explicit operator Decimal(float value);
  public static explicit operator Decimal(double value);
  ...

  public static explicit operator int(Decimal value);
  public static explicit operator long(Decimal value);
  public static explicit operator float(Decimal value);
  public static explicit operator double(Decimal value);
  ...
}

Although allowed and useful in some situations, operator overloads should be used cautiously. Operator overloading is often abused, such as when framework designers use operators for operations that should be simple methods. The following guidelines should help you decide when and how to use operator overloading.

Images AVOID defining operator overloads, except in types that should feel like primitive (built-in) types.

Images CONSIDER defining operator overloads in a type that should feel like a primitive type.

For example, System.String has operator== and operator!= defined.

Images DO define operator overloads in structs that represent numbers (such as System.Decimal).

Images DO NOT be cute when defining operator overloads.

Operator overloading is useful in cases in which it is immediately obvious what the result of the operation will be. For example, it makes sense to be able to subtract one DateTime from another DateTime and get a TimeSpan. However, it is not appropriate to use the logical union operator to combine two database queries, or to use the shift operator to write to a stream.

Images DO NOT provide operator overloads unless at least one of the operands is of the type defining the overload.

In other words, operators should operate on types that define them. The C# compiler enforces this guideline.

Click here to view code image

public struct RangedInt32 {
 public static RangedInt32 operator-(RangedInt32 left, RangedInt32 right);
 public static RangedInt32 operator-(RangedInt32 left, int right);
 public static RangedInt32 operator-(int left, RangedInt32 right);

 // The following would violate the guideline and in fact does not
 // compile in C#.
 // public static RangedInt32 operator-(int left, int right);
}

Images DO overload operators in a symmetric fashion.

For example, if you overload operator==, you should also overload operator!=. Similarly, if you overload operator<, you should also overload operator>, and so on.

Images DO provide methods with friendly names that correspond to each overloaded operator.

Many languages do not support operator overloading. For this reason, it is recommended that types that overload operators include a secondary method with an appropriate domain-specific name that provides equivalent functionality. The operator method is generally implemented as a call to the named method, especially if the named method is virtual. The following example illustrates this point.

Click here to view code image

public struct DateTimeOffset {
  public static TimeSpan operator-(
    DateTimeOffset left, DateTimeOffset right)
    => left.Subtract(right);

  public TimeSpan Subtract(DateTimeOffset value) { ... }
 }

Table 5-1 lists the .NET operators and the corresponding friendly method names.

Table 5-1: Operators and Corresponding Method Names

C# Operator Symbol

Metadata Name

Friendly Name

N/A

op_Implicit

To<TypeName>/From<TypeName>

N/A

op_Explicit

To<TypeName>/From<TypeName>

+ (binary)

op_Addition

Add

- (binary)

op_Subtraction

Subtract

* (binary)

op_Multiply

Multiply

/

op_Division

Divide

%

op_Modulus

Mod or Remainder

^

op_ExclusiveOr

Xor

& (binary)

op_BitwiseAnd

BitwiseAnd

|

op_BitwiseOr

BitwiseOr

&&

op_LogicalAnd

And

||

op_LogicalOr

Or

=

op_Assign

Assign

<<

op_LeftShift

LeftShift

>>

op_RightShift

RightShift

N/A

op_SignedRightShift

SignedRightShift

N/A

op_UnsignedRightShift

UnsignedRightShift

==

op_Equality

Equals

!=

op_Inequality

Equals

>

op_GreaterThan

CompareTo

<

op_LessThan

CompareTo

>=

op_GreaterThanOrEqual

CompareTo

<=

op_LessThanOrEqual

CompareTo

*=

op_MultiplicationAssignment

Multiply

-=

op_SubtractionAssignment

Subtract

^=

op_ExclusiveOrAssignment

Xor

<<=

op_LeftShiftAssignment

LeftShift

%=

op_ModulusAssignment

Mod

+=

op_AdditionAssignment

Add

&=

op_BitwiseAndAssignment

BitwiseAnd

|=

op_BitwiseOrAssignment

BitwiseOr

,

op_Comma

Comma

/=

op_DivisionAssignment

Divide

--

op_Decrement

Decrement

++

op_Increment

Increment

- (unary)

op_UnaryNegation

Negate

+ (unary)

op_UnaryPlus

Plus

~

op_OnesComplement

OnesComplement

5.7.1 Overloading Operator ==

Overloading operator == is quite complicated. The semantics of the operator need to be compatible with several other members, such as Object.Equals. For more information on this subject, see sections 8.9.1 and 8.13.

5.7.2 Conversion Operators

Conversion operators are unary operators that allow conversion from one type to another. The operators must be defined as static members on either the operand or the return type. There are two types of conversion operators: implicit and explicit.

Click here to view code image

public struct RangedInt32 {
  public static implicit operator int(RangedInt32 value){ ... }
  public static explicit operator RangedInt32(int value) { ... }
  ...
}

Images DO NOT provide a conversion operator if such a conversion is not clearly expected by the end users.

Ideally, you should have some user research data showing that the conversion is expected, or some prior art examples where a similar type needed such conversion.

Images DO NOT define conversion operators outside of a type’s domain.

For example, Int32, Int64, Double, and Decimal are all numeric types, whereas DateTime is not. Therefore, there should be no conversion operator to convert Int64 (long) to a DateTime. A constructor is preferred in such a case.

Click here to view code image

public struct DateTime {
  public DateTime(long ticks){ ... }
}

Images DO NOT provide an implicit conversion operator if the conversion is potentially lossy.

For example, there should not be an implicit conversion from Double to Int32 because Double has a wider range than Int32. An explicit conversion operator can be provided even if the conversion is potentially lossy.

Images DO NOT provide an implicit conversion operator if the conversion requires nontrivial work.

Since implicit conversions are usually not visible in calling code, the potential performance costs of the method will be hidden from anyone reading the code. If a conversion requires more than a small amount of constant-time work, it should instead be exposed as a named method.

Images DO NOT throw exceptions from implicit conversion operators.

It is very difficult for end users to understand what is happening, because they might not be aware that a conversion is taking place.

Images DO throw System.InvalidCastException if a call to a cast operator results in a lossy conversion and the contract of the operator does not allow lossy conversions.

Click here to view code image

public static explicit operator RangedInt32(long value) {
  if (value < Int32.MinValue || value > Int32.MaxValue) {
      throw new InvalidCastException();
   }
   return new RangedInt32((int)value, Int32.MinValue, Int32.MaxValue);
}

5.7.3 Inequality Operators

The inequality operators (<, <=, >, >=) allow for concise mathematical expressions to be written in code. The use of these operators on custom types should be consistent with the built-in operators. The strict inequality operator (!=) is specifically associated with the equality operator (==), discussed in section 8.13.

Images DO only implement inequality operators on types that implement IComparable<T>.

Images DO implement inequality operators consistent with the IComparable<T> implementation.

Because IComparable<T> and the inequality operators are performing similar roles, anything defining the operators automatically has an easy basis for implementing the interface.

Click here to view code image

public static bool operator <(SomeType left, SomeType right) =>
   left.CompareTo(right) < ʘ;

Images DO implement operator <= on types that implement operator < and IEquatable<T>.

When both “less than” and “equal” are defined, then “less than or equal” should be well defined.

Images DO return a Boolean value from custom inequality operators.

Images DO define inequality operators consistent with their mathematical properties.

Custom inequality operators should all behave as they do on built-in numeric types:

  • The transitive property: If a < b and b < c, then a < c.

  • The reversal property: If a < b, then b > a.

  • The irreflexive property: If a < a is false, then a > a is false.

  • The asymmetric property: If a < b is true, then b < a is false.

Following this guideline allows developers using your operators to write code, and their reviewers to read it, and apply the same logical analysis they would to your type as they would to the built-in numeric types.

Using the inequality operators for a projective map, or for any purpose other than Boolean mathematical comparisons, makes code harder to reason about.

Click here to view code image

// Almost everyone believes that the type of x here is Boolean.
// Therefore it should be for any inequality operator you define.
var x = a < b;

Images AVOID defining inequality operators against different types.

The reversal property of inequality says that for any a < b that returns true, b > a should also return true. Therefore, to be consistent with the expected inequality behaviors, you need to define both ThisType < ThatType and ThatType > ThisType. The asymmetric property, and the guidance for implementing operators symmetrically, means that you also have to define ThisType > ThatType and ThatType < ThisType. The transitive property says you also have to define ThisType > ThisType and ThisType < ThisType if you haven’t already. Finally, the guidance about implementing IComparable<T> says that both ThisType and ThatType should be IComparable<T> against each other (and themselves)—a task that can be accomplished only when ThisType and ThatType are in the same assembly.

Rather than implementing six different operators (and even more if <= is supported), two interfaces, and two methods, consider making use of a conversion operator (section 5.7.2) or a method or property that converts one type to another. For example, DateTimeOffset and DateTime can be compared via inequality:

Click here to view code image

DateTimeOffset a = GetDateTimeOffset();
DateTime b = GetDateTime();

// This compiles and behaves as expected
if (a < b) {
  ...
}

DateTimeOffset doesn’t define an operator < to compare against a DateTime, but instead has an implicit conversion operator to convert a DateTime into a DateTimeOffset and an operator < to compare two DateTimeOffset values:

Click here to view code image

public partial struct DateTimeOffset {
 public static implicit operator DateTimeOffset(DateTime dateTime) {...}

 public static operator <(DateTimeOffset left, DateTimeOffset right){...}
}

DateTimeOffset values can be compared to DateTime values (the other direction) by using the DateTimeOffset.UtcDateTime property.

Click here to view code image

public partial struct DateTimeOffset {
 public DateTime UtcDateTime { get { ... } }
}

5.8 Parameter Design

This section provides broad guidelines on parameter design, including sections with guidelines for checking arguments. In addition, you should refer to the parameter naming guidelines in Chapter 3.

Images DO use the least derived parameter type that provides the functionality required by the member.

For example, suppose you want to design a method that enumerates a collection and prints each item to the console. Such a method should take IEnumerable<T> as the parameter, not List<T> or IList<T>, for example.

Click here to view code image

public void WriteItemsToConsole(IEnumerable<object> items) {
   foreach(object item in items) {
     Console.WriteLine(item.ToString());
   }
}

None of the specific IList<T> members needs to be used inside the method. Typing the parameter as IEnumerable<T> allows the end user to pass collections that implement only IEnumerable<T> and not IList<T>.

Images DO NOT use reserved parameters.

If more input to a member is needed in some future version, a new overload can be added. For example, it would be bad to reserve a parameter as follows:

Click here to view code image

public void Method(SomeOption option, object reserved);

It is better to simply add a parameter to a new overload in a future version, as shown in the following example:

Click here to view code image

public void Method(SomeOption option);

// added in a future version
public void Method(SomeOption option, string path);

Images DO NOT have publicly exposed methods that take pointers, arrays of pointers, or multidimensional arrays as parameters.

Pointers and multidimensional arrays are relatively difficult to use properly. In almost all cases, APIs can be redesigned to avoid taking these types as parameters.

Images DO place all out parameters following all of the by-value and ref parameters (excluding parameter arrays), even if it results in an inconsistency in parameter ordering between overloads (see section 5.1.1).

The out parameters can be seen as extra return values, and grouping them together makes the method signature easier to understand. For example:

Click here to view code image

public struct DateTimeOffset {
  public static bool TryParse(string input, out DateTimeOffset result);
  public static bool TryParse(string input, IFormatProvider
formatProvider, DateTimeStyles styles, out DateTimeOffset result);
}

Images DO be consistent in naming parameters when overriding members or implementing interface members.

This better communicates the relationship between the methods.

Click here to view code image

public interface IComparable<T> {
  int CompareTo(T other);
}

public class Nullable<T> : IComparable<Nullable<T>> {
  // correct
  public int CompareTo(Nullable<T> other) { ... }
  // incorrect
  public int CompareTo(Nullable<T> nullable) { ... }
}

public class Object {
  public virtual bool Equals(object obj) { ... }
}

public class String {
  // correct; the parameter to the base method is called 'obj'
  public override bool Equals(object obj) { ... }

  // incorrect; the parameter should be called 'obj'
   public override bool Equals(object value) { ... }
}

5.8.1 Choosing Between Enum and Boolean Parameters

A framework designer often must decide when to use enums and when to use Booleans for parameters. In general, you should favor using enums if doing so improves the readability of the client code, especially in commonly used APIs. If using enums would add unneeded complexity and actually hurt readability, or if the API is very rarely used, use Booleans instead.

Images DO use enums if a member would otherwise have two or more Boolean parameters.

Enums are much more readable when it comes to books, documentation, source code reviews, and so on. For example, look at the following method call:

Click here to view code image

Stream stream = File.Open("foo.txt", true, false);

This call gives the reader no context within which to understand the meaning of true and false. The call would be much more usable if it were to use enums, as follows:

Click here to view code image

Stream stream = File.Open("foo.txt", CasingOptions.CaseSensitive,
FileMode.Open);

Images DO NOT use Boolean parameters unless you are absolutely sure there will never be a need for more than two values.

Enums give you some room for future addition of values, but you should be aware of all the implications of adding values to enums, as described in section 4.8.2.

Images CONSIDER using Booleans for constructor parameters that are truly two-state values and are simply used to initialize Boolean properties.

5.8.2 Validating Arguments

Rigorous checks on arguments passed to members are a crucial element of modern reusable libraries. Although argument checks might have a slight impact on performance, end users are in general willing to pay the price for the benefit of better error reporting, which becomes possible if arguments are validated as high on the call stack as possible.

Images DO validate arguments passed to public, protected, or explicitly implemented members. Throw System.ArgumentException, or one of its subclasses, if the validation fails.

Click here to view code image

public class StringCollection : IList {
   int IList.Add(object item){
       string str = item as string;
       if(str==null) throw new ArgumentNullException(...);
       return Add(str);
   }
}

Note that the actual validation does not necessarily have to happen in the public or protected member itself. It could happen at a lower level in some private or internal routine. The main point is that the entire surface area that is exposed to the end users checks the arguments.

Images DO throw ArgumentNullException if a null argument is passed and the member does not support null arguments.

Images DO validate enum parameters.

Do not assume enum arguments will be in the range defined by the enum. The CLR allows casting any integer value into an enum value even if the value is not defined in the enum.

Click here to view code image

public void PickColor(Color color) {
  if(color > Color.Black || color < Color.White){
    throw new ArgumentOutOfRangeException(...);
  }
...
}

Images DO NOT use Enum.IsDefined for enum range checks.

Images DO be aware that mutable arguments might have changed after they were validated.

If the member is security-sensitive, you are encouraged to make a copy and then validate and process the argument.

5.8.3 Parameter Passing

From the perspective of a framework designer, there are four main groups of parameters: by-value parameters, ref parameters, in (ref readonly) parameters, and out parameters.

When an argument is passed through a by-value parameter, the member receives a copy of the actual argument passed in. If the argument is a value type, a copy of the argument is put on the stack. If the argument is a reference type, a copy of the reference is put on the stack. Most popular CLR languages, such as C#, VB.NET, and C++, default to passing parameters by value.

Click here to view code image

public void Add (object value) {...}

When an argument is passed through a ref parameter, the member receives a reference to the actual argument passed in. If the argument is a value type, a reference to the argument is put on the stack. If the argument is a reference type, a reference to the reference is put on the stack. Ref parameters can be used to allow the member to modify arguments passed by the caller.

Click here to view code image

public static void Swap(ref object obj1, ref object obj2){
  object temp = obj1;
  obj1 = obj2;
  obj2 = temp;
}

In parameters are also called ref readonly parameters, and are similar to “normal” ref parameters. The most significant difference from ref parameters is that the receiving method cannot assign a replacement value through the parameter. For reference types, there is no functional difference between the default pass-by-reference and using the in parameter modifier—but using in makes all member accesses slower. For value types, a method cannot assign fields of an in parameter, and the compiler will make defensive copies of the parameter before invoking methods or properties—unless the type, method, or property accessor was declared with the readonly modifier (section 4.7). These defensive copies ensure that the called method cannot directly or indirectly modify the caller’s value.

Out parameters are similar to ref parameters, with some small differences. Such a parameter is initially considered unassigned and cannot be read in the member body before it is assigned some value. Also, the parameter has to be assigned some value before the member returns. For example, the following example will not compile and generates the compiler error “Use of unassigned out parameter ‘uri.’”

Click here to view code image

public class Uri {
  public bool TryParse(string uriString, out Uri uri){
    Trace.WriteLine(uri);
    ...
  }
}

Images AVOID using out or ref parameters except when implementing patterns that require them, such as the Try pattern (section 7.5.2).

Using out or ref parameters requires experience with pointers, understanding how value types and reference types differ, and handling methods with multiple return values. Also, the difference between out and ref parameters is not widely understood. Framework architects designing for a general audience should not expect users to master the challenge of working with out or ref parameters.

Images DO NOT pass reference types by reference (ref or in).

There are some limited exceptions to the rule, such as when a method can be used to swap references. There is no reason to pass a reference type with the in modifier (ref readonly semantics).

Click here to view code image

public static class Reference {
  public void Swap<T>(ref T obj1, ref T obj2){
    T temp = obj1;
    obj1 = obj2;
    obj2 = temp;
  }
}

Images DO NOT pass value types by read-only reference (in).

The main benefit of passing a value type by read-only reference is to reduce the cost of copying the value when the type is large. Since the guidance for value types is that they should be small (section 4.2), the performance benefit is slight. Conversely, if your method reads a property or invokes a method—without the appropriate readonly modifier—on an in parameter, you end up paying the cost silently. Once the compiler has made two defensive copies (or one copy if the type is small), your API now pays a performance penalty for the modifier, rather than receiving a performance boost.

This performance penalty risk, combined with some CLR languages having unpleasant syntax for passing by reference, means you should rarely, if ever, use the in modifier in public or protected methods.

5.8.4 Members with Variable Number of Parameters

Members that can take a variable number of arguments are expressed by providing an array parameter. For example, String provides the following method:

Click here to view code image

public class String {
  public static string Format(string format, object[] parameters);
}

A user can then call the String.Format method, as follows:

Click here to view code image

String.Format("File {0} not found in {1}",
 new object[]{filename,directory});

Adding the C# params keyword to an array parameter changes the parameter to a so-called params array parameter and provides a shortcut to creating a temporary array.

Click here to view code image

public class String {
   public static string Format(string format, params object[] parameters);
}

Doing this allows the user to call the method by passing the array elements directly in the argument list.

Click here to view code image

String.Format("File {0} not found in {1}",filename,directory);

Note that the params keyword can be added only to the last parameter in the parameter list.

Images CONSIDER adding the params keyword to array parameters if you expect the end users to pass arrays with a small number of elements.

If lots of elements will likely be passed in common scenarios, users will probably not pass these elements inline anyway, so the params keyword is not necessary.

Images AVOID using params arrays if the caller would almost always have the input already in an array.

For example, members with byte array parameters would almost never be called by passing individual bytes. For this reason, byte array parameters in .NET do not use the params keyword.

Images DO NOT use params arrays if the array is modified by the member taking the params array parameter.

Because many compilers turn the arguments to the member into a temporary array at the call site, the array might be a temporary object, and therefore any modifications to the array will be lost.

Images CONSIDER using the params keyword in a simple overload, even if a more complex overload could not use it.

Ask yourself if users would value having the params array in one overload, even if it wasn’t in all overloads. Consider the following overloaded method:

Click here to view code image

public class Graphics {
  FillPolygon(Brush brush, params Point[] points) { ... }
  FillPolygon(Brush brush, Point[] points, FillMode fillMode) {
    ...
  }
}

The array parameter of the second overload is not the last parameter in the parameter list, so it cannot use the params keyword. This does not mean that the keyword should not be used in the first overload, where it is the last parameter. If the first overload is used often, users will appreciate the addition.

Images DO try to order parameters to make it possible to use the params keyword.

Consider the following overloads on PropertyDescriptorCollection:

Click here to view code image

Sort()
Sort(IComparer comparer)
Sort(string[] names, IComparer comparer)
Sort(params string[] names)

Because of the order of parameters on the third overload, the opportunity to use the params keyword has been lost. The parameters could be reordered to allow for this keyword in both overloads.

Click here to view code image

Sort()
Sort(IComparer comparer)
Sort(IComparer comparer, params string[] names)
Sort(params string[] names)

Images CONSIDER providing special overloads and code paths for calls with a small number of arguments in extremely performance-sensitive APIs.

This makes it possible to avoid creating array objects when the API is called with a small number of arguments. Name the parameters by taking a singular form of the array parameter and adding a numeric suffix.

Click here to view code image

void Format (string formatString, object arg1)
  void Format (string formatString, object arg1, object arg2)
...
  void Format (string formatString, params object[] args)

You should do this only if you are going to treat the entire code path as a special case, not just create an array and call the more general method.

Images DO be aware that null could be passed as a params array argument.

You should validate that the array is not null before processing it.

Click here to view code image

static void Main() {
  Sum(1, 2, 3, 4, 5); //result == 15
  Sum(null);
}
static int Sum(params int[] values) {
  if(values==null) throw ArgumentNullException(...);
  int sum = ʘ;
  foreach (int value in values) {
    sum += value;
  }
  return sum;
}

Images DO NOT use the varargs methods, otherwise known as the ellipsis.

Some CLR languages, such as C++, support an alternative convention for passing variable parameter lists called varargs methods. This convention should not be used in frameworks, because it is not CLS-compliant.

5.8.5 Pointer Parameters

In general, pointers should not appear in the public surface area of a well-designed managed code framework. Most of the time, pointers should be encapsulated. However, pointers are sometimes required for interoperability reasons, and using pointers in such cases is appropriate.

The Span<T> and ReadOnlySpan<T> types unify native memory and managed arrays, and can be pinned to a pointer via the fixed keyword in C#. Using spans instead of pointers is preferred, when possible. For more information on spans, see section 9.12.

Images DO provide an alternative for any member that takes a pointer argument, because pointers are not CLS-compliant.

Click here to view code image

[CLSCompliant(false)]
public unsafe int GetBytes(char* chars, int charCount,
 byte* bytes, int byteCount);
public int GetBytes(char[] chars, int charIndex, int charCount,
 byte[] bytes, int byteIndex, int byteCount)

// Consider Span<T> / ReadOnlySpan<T> instead of
// (or in addition to) methods that take pointers
public int GetBytes(ReadOnlySpan<char> source, Span<byte> destination);

Images AVOID doing expensive argument checking for pointer arguments.

In general, argument checking is well worth the cost, but for APIs that are performance-critical enough to require using pointers, the overhead is often not worth it.

Images DO follow common pointer-related conventions when designing members with pointers.

For example, there is no need to pass the start index, because simple pointer arithmetic can be used to accomplish the same result.

Click here to view code image

//Bad practice
public unsafe int GetBytes(char* chars, int charIndex, int charCount,
 byte* bytes, int byteIndex, int byteCount)

//Better practice
public unsafe int GetBytes(char* chars, int charCount,
 byte* bytes, int byteCount)

//Example call site
GetBytes(chars + charIndex, charCount, bytes + byteIndex, byteCount);

5.9 Using Tuples in Member Signatures

Loosely speaking, a tuple is any ordered collection of instances of heterogeneous types. If we consider only strongly typed (section 2.2.3.3) heterogeneous structures, then C# (and other .NET languages) have four different mechanisms for providing a tuple under this definition: the System.Tuple<T1, T2> class (including other generic counts), the System.ValueTuple<T1, T2> struct (including other generic counts), the language feature “named tuples,” and descriptive types.

To highlight the main characteristics of these four alternatives, we show how each of them can define a simple person as an entity with a first name, a last name, and an age.

  1. System.Tuple<T1, ...> classes

    Click here to view code image

    public Tuple<string, string, int> GetPerson() {
        return Tuple.Create("John", "Smith", 32);
    }
    
  2. System.ValueTuple<T1, ...> structs

    Click here to view code image

    public ValueTuple<string, string, int> GetPerson() {
        return ValueTuple.Create("John", "Smith", 32);
    }
    
  3. Named tuples

    Click here to view code image

    public (string FirstName, string LastName, int Age) GetPerson() {
        return ("John", "Smith", 32);
    }
    
  4. Descriptive types

    Click here to view code image

    public struct Person {
        public string FirstName { get; }
        public string LastName { get; }
        public int Age { get; }
    
        public Person(string firstName, string lastName, int age) {
            FirstName = firstName;
            LastName = lastName;
            Age = age;
        }
    }
    public Person GetPerson() {
        return new Person("John", "Smith", 32);
    }
    

Declaration is only one part of the story for a value. For each of the previous declarations, we will now look at building a message printing a person’s target heart rate using the heuristic of 220 beats per minute minus the person’s age.

  1. System.Tuple<T1, ...> classes

    Click here to view code image

    Tuple<string, string, int> person = GetPerson();
    string message = $"{person.Item1} {person.Item2}'s target heart
    rate is {22ʘ - person.Item3}."
    
  2. System.ValueTuple<T1, ...> structs

    Click here to view code image

    ValueTuple<string, string, int> person = GetPerson();
    string message = $"{person.Item1} {person.Item2}'s target heart
    rate is {22ʘ - person.Item3}."
    
  3. Named tuples

    Click here to view code image

    (string FirstName, string LastName, int Age) person = GetPerson();
    string message = $"{person.FirstName} {person.LastName}'s target
    heart rate is {22ʘ - person.Age}."
    
    // This syntax is also available to named tuples
    //string message = $"{person.Item1} {person.Item2}'s target heart
    rate is {22ʘ - person.Item3}."
    
  4. Descriptive types

    Click here to view code image

    Person person = GetPerson();
    string message = $"{person.FirstName} {person.LastName}'s target
    heart rate is {22ʘ - person.Age}."
    

When using System.Tuple<T1, T2, T3> or System.ValueTuple<T1, T2, T3>, the members exposed on the enumerated object are “Item1,” “Item2,” and “Item3.” These names do not help convey the purpose of the values (which is more problematic when two or more of the element types are the same). Conversely, for both the descriptive type and the named tuple. the names shown via IntelliSense are the much more expressive “FirstName,” “LastName,” and “Value.”

The descriptive type has additional benefits over the alternatives, not all of which would apply to this example.

  • The constructor could have data validation, or other custom logic.

  • Methods can be declared on the type.

  • Additional fields and properties can be added in the future.

  • Program analyzers can track the type to see what places produce one, and what places accept one as input.

  • The descriptive type provides a target for documentation.

  • The descriptive names are available in all CLR languages.

The .NET runtime considers two generic types with the same base name but a different number of generic parameters to be different types, so changing from returning Tuple<T1, T2> to Tuple<T1, T2, T3> is just as much a breaking change as changing a return type from String to Int32.

Images DO prefer descriptive types over tuples, System.Tuple<T1, ...>, and System.ValueTuple<T1, ...>; they improve discoverability and can better evolve over time.

Images DO prefer named tuples over unnamed tuples (including System.Tuple<T1, ...> and System.ValueTuple<T1, ...>).

The previous section shows why descriptive types are better than named tuples in public API, as well as why named tuples are better than unnamed tuples. However, occasionally a method is so specialized that it would never make sense to amend the returned data, a custom descriptive type would only ever be deconstructed into locals, and the method name really is sufficient for documentation. When all of these conditions are true for a method’s return type, then a named tuple might make more sense. For example, .NET Standard 2.1 added System.Range, which has a utility method to compute the total length and initial offset for a range:

Click here to view code image

public partial struct Range {
   public (int Offset, int Length) GetOffsetAndLength(int length);
}

Images DO name elements of a named tuple using PascalCase, as if they were properties.

Named tuples in C# allow access to their elements via the same syntax as accessing a property or field on a descriptive type, so they should be named accordingly.

Images DO NOT use tuples with more than three fields.

Tuples are a convenient way to represent a data pair or triplet that will be separated into the individual fields, such as via the C# deconstruction language feature. Once more than three fields are being returned together, it seems more likely that the values will stay grouped together, and be passed to other methods or stored in a look-up table—a role better suited to descriptive types.

Images DO NOT use tuples as method parameters.

When a tuple is used as a parameter, it introduces extra syntax for both the method declaration and the caller, but does not provide much value in return.

Accepting a collection based on tuples (such as (string FirstName, string LastName, int Age)[] people) is more justifiable than accepting a single tuple value parameter. However, operating on a collection suggests a level of complexity that would be better served by descriptive types—validating constructors, instance methods, and the ability to add to the type without a breaking change. Callers of the method may also be forced to use awkward syntax to get their data into the right shape.

This guideline does not mean that library authors should go out of their way to prohibit named tuples or unnamed tuples to be specified as generic type parameters, even though that could cause a violation after generic substitution.

Click here to view code image

// OK
public static T Max(IComparer<T> comparer, T first, T second, T third) {...}

// OK
var first = (Base: 1, Exponent: 2);
var second = (Base: 2, Exponent: 1);
var third = (Base: 3, Exponent: -5);
(int Base, int Exponent) max = Max(comparer, first, second, third);

// Not OK if explicitly defined this way
public static (int Base, int Exponent) Max(
 IComparer<(int Base, int Exponent)> comparer, ...) { ... }

Images DO NOT define extension methods over tuples.

This is a special case of not using tuples as method parameters, but is worth mentioning in its own right.

Suppose we were to define a method to calculate the dot product of two tuples used to represent a vector on the X-Y plane:

Click here to view code image

public static int DotProduct(this (int X, int Y) first,
(int X, int Y) second) {
   return first.X * second.X + first.Y * second.Y;
}

This extension method would also show up in IntelliSense on the (int Base, int Exponent) variables from the previous example, and the following code would compile:

Click here to view code image

(int Base, int Exponent) value = (2, 2ʘ);
ValueTuple<int, int> otherValue = ValueTuple.Create(5, 7);
int dotProduct = value.DotProduct(otherValue);

This is because the C# language (and VB.NET) considers all tuples, as well as System.ValueTuple<T1,...>, to be the same type if their ordered list of element types is the same.

Images CONSIDER adding an appropriate Deconstruct method to any type that was chosen as an alternative to a tuple.

By adding a void method on our Person class named Deconstruct, we can use the C# 7.0 deconstruction feature:

Click here to view code image

public partial struct Person {
   public void Deconstruct(out string firstName, out string lastName,
out int age) {
       firstName = FirstName;
       lastName = LastName;
       age = Age;
   }
}

The Deconstruct method enables us to build our target heart rate message with the following code:

Click here to view code image

(string firstName, string lastName, int age) = GetPerson();
string message = $"{firstName} {lastName}'s target heart rate is
{22ʘ - age}."

This same code works for all four representations of the person because System.Tuple<T1, ...>, System.ValueTuple<T1, ...>, and named tuples all have similar Deconstruct methods and the deconstruction language feature does not require any similarity in the out parameter names and the local variable names.

The deconstruction syntax is especially nice for tuples, as their lack of methods means that any logic operating on a tuple has to first break it down to some (or all) of its element values. If a tuple was a candidate for the return type of a method, then even for the descriptive type, a likely common operation for callers will be to break the type down to its element values.

Summary

This chapter offers comprehensive guidelines for general member design. As the annotations suggest, member design is one of the most complex parts of designing a framework. This is a natural consequence of the richness of concepts related to member design.

The next chapter covers design issues relating to extensibility.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.227.228.95