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.
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.
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:
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.
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.
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.
public class Type { public MethodInfo GetMethod(string name); //ignoreCase = false public MethodInfo GetMethod(string name, Boolean ignoreCase); }
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.
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) { ... } }
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.
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.
DO make only the longest overload virtual (if extensibility is required). Shorter overloads should simply call through to a longer overload.
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.
DO NOT use ref
, out
, or in
modifiers to overload members.
For example, you should not do the following:
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.
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:
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:
public static class Console { public void WriteLine(long value){ ... } public void WriteLine(int value){ ... } }
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:
if (geometry==null) DrawGeometry(brush, pen); else DrawGeometry(brush, pen, geometry);
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:
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.
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");
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.
public static BigInteger Parse( ReadOnlySpan<char> value, NumberStyles style = NumberStyles.Integer, IFormatProvider provider = null) { ... } ... BigInteger val = BigInteger.Parse(input, provider: formatCulture);
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.
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) { ... }
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.
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.
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.
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.
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.
// 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) { ... }
DO NOT have two overloads of a method with “compatible” required parameters that both use default parameters.
AVOID having two overloads of the same method that both use default parameters.
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.
// 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) { ... }
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.
// 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) { ... }
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:
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.
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.
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.
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.
public class StringCollection : IList { public string this[int index]{ ... } object IList.this[int index] { ... } ... }
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
.
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.
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.
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.
[Serializable] public class List<T> : ISerializable { ... void ISerializable.GetObjectData( SerializationInfo info, StreamingContext context) { GetObjectData(info,context); } protected virtual void GetObjectData( SerializationInfo info, StreamingContext context) { ... } }
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.
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.
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.
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.
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.
public Customer { public Customer(string name){ this.name = name; } public string Name { get { return this.name; } } private string name; }
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:
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.
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>
.
public ReadOnlyCollection<Employee> Employees { get { return roEmployees; } } private Employee[] employees; private ReadOnlyCollection<Employee> roEmployees;
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.
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.
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
.
DO provide sensible default values for all properties, ensuring that the defaults do not result in a security hole or terribly inefficient code.
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.
DO preserve the previous value if a property setter throws an exception.
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.
An indexed property is a special property that can have parameters and can be called with special syntax similar to array indexing.
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.
CONSIDER using indexers to provide access to data stored in an internal array.
CONSIDER providing indexers on types representing collections of items.
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
.
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
.
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.
public sealed class String { [System.Runtime.CompilerServices.IndexerNameAttribute("Chars")] public char this[int index] { get { ... } } ... }
DO NOT provide both an indexer and methods that are semantically equivalent.
In the following example, the indexer should be changed to a method.
// Bad design public class Type { [System.Runtime.CompilerServices.IndexerNameAttribute("Members")] public MemberInfo this[string memberName]{ ... } public MemberInfo GetMember(string memberName, Boolean ignoreCase){ ... } }
DO NOT provide more than one family of overloaded indexers in one type.
This is enforced by the C# compiler.
DO NOT use nondefault indexed properties.
This is enforced by the C# compiler.
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.
// 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) { ... } }
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.
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.
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.
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.
There are two kinds of constructors: type constructors and instance constructors.
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).
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.
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.
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:
//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");
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.
public class EventLog { public EventLog(string logName){ this.LogName = logName; } public string LogName { get { ... } set { ... } } }
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.
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.
DO throw exceptions from instance constructors, if appropriate.
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.
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:
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.
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).
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
).
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.
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.
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.
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.
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.
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.
// 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); } }
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.
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.
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(); }
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.”
DO use System.EventHandler<T>
instead of manually creating new delegates to be used as event handlers.
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.
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.
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.
public class AlarmRaisedEventArgs : EventArgs { public DateTimeOffset AlarmTime { get; } }
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.
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.
protected virtual void OnAlarmRaised(AlarmRaisedEventArgs e){ AlarmRaised?.Invoke(this, e); }
DO NOT pass null
as the sender when raising a nonstatic event.
DO pass null
as the sender when raising a static event.
EventHandler<EventArgs> handler = ...; if (handler!=null) handler(null,...);
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
.
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:
void ClosingHandler(object sender, CancelEventArgs e) { e.Cancel = true; }
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.
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.
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.
public struct Int32 { public const int MaxValue = ʘx7fffffff; public const int MinValue = unchecked((int)ʘx8ʘʘʘʘʘʘʘ); }
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.
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:
public static Encoding UTF8 { get { return _utf8Encoding ??= new UTF8Encoding(); } }
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.
public class SomeType { public static readonly int[] Numbers = new int[1ʘ]; } ... SomeType.Numbers[5] = 1ʘ; // changes a value in the array
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.
public static class StringExtensions { public static bool IsPalindrome(this string s){ ... } }
This extension method can be called as follows:
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.
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.
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.
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>
.
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.
// 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>
.
public static bool IsWhiteSpace(this ReadOnlySpan<char> span) {...}
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.
// 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();
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:
// 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.
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.
// 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> { ... } }
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.
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.
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){...} } }
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.
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.
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.
public static partial class CollectionDisposer { public static void DisposeAll<T>( this List<T> list) where T : IDisposable { ... } }
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.
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
.
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.
AVOID defining operator overloads, except in types that should feel like primitive (built-in) types.
CONSIDER defining operator overloads in a type that should feel like a primitive type.
For example, System.String
has operator==
and operator!=
defined.
DO define operator overloads in structs that represent numbers (such as System.Decimal
).
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.
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.
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); }
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.
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.
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 |
|
|
N/A |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
N/A |
|
|
N/A |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.
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.
public struct RangedInt32 { public static implicit operator int(RangedInt32 value){ ... } public static explicit operator RangedInt32(int value) { ... } ... }
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.
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.
public struct DateTime { public DateTime(long ticks){ ... } }
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.
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.
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.
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.
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); }
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.
DO only implement inequality operators on types that implement IComparable<T>
.
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.
public static bool operator <(SomeType left, SomeType right) => left.CompareTo(right) < ʘ;
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.
DO return a Boolean value from custom inequality operators.
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.
// 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;
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:
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:
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.
public partial struct DateTimeOffset { public DateTime UtcDateTime { get { ... } } }
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.
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.
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>
.
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:
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:
public void Method(SomeOption option); // added in a future version public void Method(SomeOption option, string path);
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.
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:
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); }
DO be consistent in naming parameters when overriding members or implementing interface members.
This better communicates the relationship between the methods.
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) { ... } }
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.
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:
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:
Stream stream = File.Open("foo.txt", CasingOptions.CaseSensitive, FileMode.Open);
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.
CONSIDER using Booleans for constructor parameters that are truly two-state values and are simply used to initialize Boolean properties.
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.
DO validate arguments passed to public, protected, or explicitly implemented members. Throw System.ArgumentException
, or one of its subclasses, if the validation fails.
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.
DO throw ArgumentNullException
if a null argument is passed and the member does not support null arguments.
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.
public void PickColor(Color color) { if(color > Color.Black || color < Color.White){ throw new ArgumentOutOfRangeException(...); } ... }
DO NOT use Enum.IsDefined
for enum range checks.
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.
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.
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.
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.’”
public class Uri { public bool TryParse(string uriString, out Uri uri){ Trace.WriteLine(uri); ... } }
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.
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).
public static class Reference { public void Swap<T>(ref T obj1, ref T obj2){ T temp = obj1; obj1 = obj2; obj2 = temp; } }
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.
Members that can take a variable number of arguments are expressed by providing an array parameter. For example, String
provides the following method:
public class String { public static string Format(string format, object[] parameters); }
A user can then call the String.Format
method, as follows:
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.
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.
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.
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.
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.
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.
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:
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.
DO try to order parameters to make it possible to use the params
keyword.
Consider the following overloads on PropertyDescriptorCollection
:
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.
Sort() Sort(IComparer comparer) Sort(IComparer comparer, params string[] names) Sort(params string[] names)
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.
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.
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.
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; }
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.
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.
DO provide an alternative for any member that takes a pointer argument, because pointers are not CLS-compliant.
[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);
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.
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.
//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);
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.
System.Tuple<T1, ...>
classes
public Tuple<string, string, int> GetPerson() { return Tuple.Create("John", "Smith", 32); }
System.ValueTuple<T1, ...>
structs
public ValueTuple<string, string, int> GetPerson() { return ValueTuple.Create("John", "Smith", 32); }
Named tuples
public (string FirstName, string LastName, int Age) GetPerson() { return ("John", "Smith", 32); }
Descriptive types
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.
System.Tuple<T1, ...>
classes
Tuple<string, string, int> person = GetPerson(); string message = $"{person.Item1} {person.Item2}'s target heart rate is {22ʘ - person.Item3}."
System.ValueTuple<T1, ...>
structs
ValueTuple<string, string, int> person = GetPerson(); string message = $"{person.Item1} {person.Item2}'s target heart rate is {22ʘ - person.Item3}."
Named tuples
(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}."
Descriptive types
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
.
DO prefer descriptive types over tuples, System.Tuple<T1, ...>
, and System.ValueTuple<T1, ...>
; they improve discoverability and can better evolve over time.
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:
public partial struct Range { public (int Offset, int Length) GetOffsetAndLength(int length); }
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.
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.
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.
// 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, ...) { ... }
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:
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:
(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.
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:
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:
(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.
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.
18.227.228.95