A type that is defined as a class is a reference type. At run time, when you declare a variable of a reference type, the variable contains the value null until you explicitly create an instance of the class by using the new operator, or assign it an object of a compatible type that may have been created elsewhere, as shown in the following example:
//Declaring an object of type MyClass.
MyClass myobj1 = new MyClass();
//Declaring another object of the same type, assigning it the value of the first object.
MyClass myobj2 = myobj1;
When the object is created, enough memory is allocated on the managed heap for that specific object, and the variable holds only a reference to the location of said object. The memory used by an object is reclaimed by the automatic memory management functionality of the CLR, which is known as garbage collection. For more information about garbage collection, see Automatic memory management and garbage collection.
Declaring classes
Classes are declared by using the
class
keyword followed by a unique identifier, as shown in the following example:
//[access modifier] - [class] - [identifier]
public class Car
{
// Fields, properties, methods and events go here...
}
An optional access modifier precedes the
class
keyword. Because public is used in this case, anyone can create instances of this class. The name of the class follows the class keyword. The name of the class must be a valid C# identifier name. The remainder of the definition is the class body, where the behavior and data are defined. Fields, properties, methods, and events on a class are collectively referred to as class members.
Creating objects
Although they're sometimes used interchangeably, a class and an object are different things. A class defines a type of object, but it isn't an object itself. An object is a concrete entity based on a class, and is sometimes referred to as an instance of a class.
Objects can be created by using the
new
keyword followed by the name of the class, like this:
Car object1 = new Car();
When an instance of a class is created, a reference to the object is passed back to the programmer. In the previous example,
object1
is a reference to an object that is based on
Car
. This reference refers to the new object but doesn't contain the object data itself. In fact, you can create an object reference without creating an object at all:
Car object2;
We don't recommend creating object references that don't refer to an object because trying to access an object through such a reference fails at run time. A reference can be made to refer to an object, either by creating a new object, or by assigning it an existing object, such as this:
Car object3 = new Car();
Car object4 = object3;
This code creates two object references that both refer to the same object. Therefore, any changes to the object made through
object3
are reflected in subsequent uses of
object4
. Because objects that are based on classes are referred to by reference, classes are known as reference types.
Constructors and initialization
The preceding sections introduced the syntax to declare a class type and create an instance of that type. When you create an instance of a type, you want to ensure that its fields and properties are initialized to useful values. There are several ways to initialize values:
⮚Accept default values
⮚Field initializers
⮚Constructor parameters
⮚Object initializers
Every .NET type has a default value. Typically, that value is 0 for number types, and null for all reference types. You can rely on that default value when it's reasonable in your app.
When the .NET default isn't the right value, you can set an initial value using a field initializer:
public class Container
{
// Initialize capacity field to a default value of 10:
private int _capacity = 10;
}
You can require callers to provide an initial value by defining a constructor that's responsible for setting that initial value:
public class Container
{
private int _capacity;
public Container(int capacity) => _capacity = capacity;
}
Beginning with C# 12, you can define a primary constructor as part of the class declaration:
public class Container(int capacity)
{
private int _capacity = capacity;
}
Adding parameters to the class name defines the primary constructor. Those parameters are available in the class body, which includes its members. You can use them to initialize fields or anywhere else where they're needed.
public class Person
{
public string LastName { get; set; }
public string FirstName { get; set; }
}
var p2 = new Person() { FirstName = "Carl", LastName = "John" };
Class inheritance
Classes fully support inheritance, a fundamental characteristic of object-oriented programming. When you create a class, you can inherit from any other class that isn't defined as sealed. Other classes can inherit from your class and override class virtual methods. Furthermore, you can implement one or more interfaces.
Inheritance is accomplished by using a derivation, which means a class is declared by using a base class from which it inherits data and behavior. A base class is specified by appending a colon and the name of the base class following the derived class name, like this:
public class Manager : Employee
{
// Employee fields, properties, methods and events are inherited
// New Manager fields, properties, methods and events go here...
}
When a class declaration includes a base class, it inherits all the members of the base class except the constructors. For more information see Inheritance.
A class in C# can only directly inherit from one base class. However, because a base class may itself inherit from another class, a class might indirectly inherit multiple base classes. Furthermore, a class can directly implement one or more interfaces. For more information, see Interfaces.
A class can be declared as abstract. An abstract class contains abstract methods that have a signature definition but no implementation. Abstract classes can't be instantiated. They can only be used through derived classes that implement the abstract methods. By contrast, a sealed class doesn't allow other classes to derive from it. For more information, see Abstract and Sealed Classes and Class Members.
A structure type (or struct type) is a value type that can encapsulate data and related functionality. You use the
struct
keyword to define a structure type:
public struct Coords
{
public Coords(double x, double y)
{
X = x;
Y = y;
}
public double X { get; }
public double Y { get; }
public override string ToString() => $"({X}, {Y})";
}
Structure types have value semantics. That is, a variable of a structure type contains an instance of the type. By default, variable values are copied on assignment, passing an argument to a method, and returning a method result. For structure-type variables, an instance of the type is copied. For more information, see Value types.
Typically, you use structure types to design small data-centric types that provide little or no behavior. For example, .NET uses structure types to represent a number (both integer and real), a Boolean value, a Unicode character, a time instance. If you're focused on the behavior of a type, consider defining a class. Class types have reference semantics. That is, a variable of a class type contains a reference to an instance of the type, not the instance itself.
Because structure types have value semantics, we recommend you define immutable structure types.
readonly struct
You use the
readonly
modifier to declare that a structure type is immutable. All data members of a
readonly
struct must be read-only as follows:
Any field declaration must have the readonly modifier
Any property, including auto-implemented ones, must be read-only or init only.
That guarantees that no member of a
readonly
struct modifies the state of the struct. That means that other instance members except constructors are implicitly readonly.
Note
In a readonly struct, a data member of a mutable reference type still
can mutate its own state. For example, you can't replace a List instance,
but you can add new elements to it.
The following code defines a readonly struct with init-only property setters:
public readonly struct Coords
{
public Coords(double x, double y)
{
X = x;
Y = y;
}
public double X { get; init; }
public double Y { get; init; }
public override string ToString() => $"({X}, {Y})";
}
readonly instance members
You can also use the readonly modifier to declare that an instance member doesn't modify the state of a struct. If you can't declare the whole structure type as readonly, use the readonly modifier to mark the instance members that don't modify the state of the struct.
Within a readonly instance member, you can't assign to structure's instance fields. However, a readonly member can call a non-readonly member. In that case, the compiler creates a copy of the structure instance and calls the non-readonly member on that copy. As a result, the original structure instance isn't modified.
Typically, you apply the readonly modifier to the following kinds of instance members:
. methods:
public readonly double Sum()
{
return X + Y;
}
You can also apply the readonly modifier to methods that override methods declared in System.Object:
public readonly override string ToString() => $"({X}, {Y})";
. properties and indexers:
private int counter;
public int Counter
{
readonly get => counter;
set => counter = value;
}
If you need to apply the readonly modifier to both accessors of a property or indexer, apply it in the declaration of the property or indexer.
You may apply the readonly modifier to a property or indexer with an init accessor:
public readonly double X { get; init; }
You can apply the readonly modifier to static fields of a structure type, but not any other static members, such as properties or methods.
The compiler may make use of the readonly modifier for performance optimizations.
Nondestructive mutation
Beginning with C# 10, you can use the with expression to produce a copy of a structure-type instance with the specified properties and fields modified. You use object initializer syntax to specify what members to modify and their new values, as the following example shows
public readonly struct Coords
{
public Coords(double x, double y)
{
X = x;
Y = y;
}
public double X { get; init; }
public double Y { get; init; }
public override string ToString() => $"({X}, {Y})";
}
public static void Main()
{
var p1 = new Coords(0, 0);
Console.WriteLine(p1); // output: (0, 0)
var p2 = p1 with { X = 3 };
Console.WriteLine(p2); // output: (3, 0)
var p3 = p1 with { X = 1, Y = 4 };
Console.WriteLine(p3); // output: (1, 4)
}
record struct
Beginning with C# 10, you can define record structure types. Record types provide built-in functionality for encapsulating data. You can define both
record
struct and
readonly record struct
types. A record struct can't be a ref struct. For more information and examples, see Records.
Inline arrays
Beginning with C# 12, you can declare inline arrays as a
struct
type:
[System.Runtime.CompilerServices.InlineArray(10)]
public struct CharBuffer
{
private char _firstElement;
}
An inline array is a structure that contains a contiguous block of N elements of the same type. It's a safe-code equivalent of the fixed buffer declaration available only in unsafe code. An inline array is a struct with the following characteristics:
In most cases, an inline array can be accessed like an array, both to read and write values. In addition, you can use the range and index operators.
There are minimal restrictions on the type of the single field. It can't be a pointer type, but it can be any reference type, or any value type. You can use inline arrays with almost any C# data structure.
Inline arrays are an advanced language feature. They're intended for high-performance scenarios where an inline, contiguous block of elements is faster than other alternative data structures. You can learn more about inline arrays from the feature speclet
Struct initialization and default values
A variable of a struct type directly contains the data for that struct. That creates a distinction between an uninitialized struct, which has its default value and an initialized struct, which stores values set by constructing it. For example consider the following code:
public readonly struct Measurement
{
public Measurement()
{
Value = double.NaN;
Description = "Undefined";
}
public Measurement(double value, string description)
{
Value = value;
Description = description;
}
public double Value { get; init; }
public string Description { get; init; }
public override string ToString() => $"{Value} ({Description})";
}
public static void Main()
{
var m1 = new Measurement();
Console.WriteLine(m1); // output: NaN (Undefined)
var m2 = default(Measurement);
Console.WriteLine(m2); // output: 0 ()
var ms = new Measurement[2];
Console.WriteLine(string.Join(", ", ms)); // output: 0 (), 0 ()
}
As the preceding example shows, the default value expression ignores a parameterless constructor and produces the default value of the structure type. Structure-type array instantiation also ignores a parameterless constructor and produces an array populated with the default values of a structure type.
The most common situation where you see default values is in arrays or in other collections where internal storage includes blocks of variables. The following example creates an array of 30 TemperatureRange structures, each of which has the default value:
// All elements have default values of 0:
TemperatureRange[] lastMonth = new TemperatureRange[30];
All of a struct's member fields must be definitely assigned when it's created because struct types directly store their data. The default value of a struct has definitely assigned all fields to 0. All fields must be definitely assigned when a constructor is invoked. You initialize fields using the following mechanisms:
You can add field initializers to any field or auto implemented property.
You can initialize any fields, or auto properties, in the body of the constructor.
Beginning with C# 11, if you don't initialize all fields in a struct, the compiler adds code to the constructor that initializes those fields to the default value. The compiler performs its usual definite assignment analysis. Any fields that are accessed before being assigned, or not definitely assigned when the constructor finishes executing are assigned their default values before the constructor body executes. If this is accessed before all fields are assigned, the struct is initialized to the default value before the constructor body executes.
public readonly struct Measurement
{
public Measurement(double value)
{
Value = value;
}
public Measurement(double value, string description)
{
Value = value;
Description = description;
}
public Measurement(string description)
{
Description = description;
}
public double Value { get; init; }
public string Description { get; init; } = "Ordinary measurement";
public override string ToString() => $"{Value} ({Description})";
}
public static void Main()
{
var m1 = new Measurement(5);
Console.WriteLine(m1); // output: 5 (Ordinary measurement)
var m2 = new Measurement();
Console.WriteLine(m2); // output: 0 ()
var m3 = default(Measurement);
Console.WriteLine(m3); // output: 0 ()
}
Every struct has a public parameterless constructor. If you write a parameterless constructor, it must be public. If a struct declares any field initializers, it must explicitly declare a constructor. That constructor need not be parameterless. If a struct declares a field initializer but no constructors, the compiler reports an error. Any explicitly declared constructor (with parameters, or parameterless) executes all field initializers for that struct. All fields without a field initializer or an assignment in a constructor are set to the default value. For more information, see the Parameterless struct constructors feature proposal note.
Beginning with C# 12, struct types can define a primary constructor as part of its declaration. Primary constructors provides a concise syntax for constructor parameters that can be used throughout the struct body, in any member declaration for that struct.
If all instance fields of a structure type are accessible, you can also instantiate it without the new operator. In that case you must initialize all instance fields before the first use of the instance. The following example shows how to do that:
public static class StructWithoutNew
{
public struct Coords
{
public double x;
public double y;
}
public static void Main()
{
Coords p;
p.x = 3;
p.y = 4;
Console.WriteLine($"({p.x}, {p.y})"); // output: (3, 4)
}
}
In the case of the built-in value types, use the corresponding literals to specify a value of the type.
Limitations with the design of a structure type
Structs have most of the capabilities of a class type. There are some exceptions, and some exceptions that have been removed in more recent versions:
A structure type can't inherit from other class or structure type and it can't be the base of a class. However, a structure type can implement interfaces.
You can't declare a finalizer within a structure type.
Prior to C# 11, a constructor of a structure type must initialize all instance fields of the type.
Passing structure-type variables by reference
When you pass a structure-type variable to a method as an argument or return a structure-type value from a method, the whole instance of a structure type is copied. Pass by value can affect the performance of your code in high-performance scenarios that involve large structure types. You can avoid value copying by passing a structure-type variable by reference. Use the ref, out, in, or ref readonly method parameter modifiers to indicate that an argument must be passed by reference. Use ref returns to return a method result by reference
struct constraint
You also use the struct keyword in the struct constraint to specify that a type parameter is a non-nullable value type. Both structure and enumeration types satisfy the struct constraint.
Conversions
For any structure type (except ref struct types), there exist boxing and unboxing conversions to and from the System.ValueType and System.Object types. There exist also boxing and unboxing conversions between a structure type and any interface that it implements.
Interfaces - define behavior for multiple types
An interface contains definitions for a group of related functionalities that a non-abstract class or a struct must implement. An interface may define
static
methods, which must have an implementation. An interface may define a default implementation for members. An interface may not declare instance data such as fields, auto-implemented properties, or property-like events.
By using interfaces, you can, for example, include behavior from multiple sources in a class. That capability is important in C# because the language doesn't support multiple inheritance of classes. In addition, you must use an interface if you want to simulate inheritance for structs, because they can't actually inherit from another struct or class.
You define an interface by using the
interface
keyword as the following example shows.
interface IEquatable
{
bool Equals(T obj);
}
The name of an interface must be a valid C# identifier name. By convention, interface names begin with a capital
I
.
Any class or struct that implements the
IEquatable
interface must contain a definition for an
Equals
method that matches the signature that the interface specifies. As a result, you can count on a class that implements
IEquatable
to contain an Equals method with which an instance of the class can determine whether it's equal to another instance of the same class.
The definition of
IEquatable
doesn't provide an implementation for Equals. A class or struct can implement multiple interfaces, but a class can only inherit from a single class.
Interfaces can contain instance methods, properties, events, indexers, or any combination of those four member types. Interfaces may contain static constructors, fields, constants, or operators. Beginning with C# 11, interface members that aren't fields may be static abstract. An interface can't contain instance fields, instance constructors, or finalizers. Interface members are public by default, and you can explicitly specify accessibility modifiers, such as
public, protected, internal, private, protected internal, or private protected
. A private member must have a default implementation.
To implement an interface member, the corresponding member of the implementing class must be public, non-static, and have the same name and signature as the interface member.
A class or struct that implements an interface must provide an implementation for all declared members without a default implementation provided by the interface. However, if a base class implements an interface, any class that's derived from the base class inherits that implementation.
The following example shows an implementation of the
IEquatable
interface. The implementing class,
Car
, must provide an implementation of the Equals method.
public class Car : IEquatable
{
public string? Make { get; set; }
public string? Model { get; set; }
public string? Year { get; set; }
// Implementation of IEquatable interface
public bool Equals(Car? car)
{
return (this.Make, this.Model, this.Year) ==
(car?.Make, car?.Model, car?.Year);
}
}
Properties and indexers of a class can define extra accessors for a property or indexer that's defined in an interface. For example, an interface might declare a property that has a get accessor. The class that implements the interface can declare the same property with both a get and set accessor. However, if the property or indexer uses explicit implementation, the accessors must match. For more information about explicit implementation
Interfaces can inherit from one or more interfaces. The derived interface inherits the members from its base interfaces. A class that implements a derived interface must implement all members in the derived interface, including all members of the derived interface's base interfaces. That class may be implicitly converted to the derived interface or any of its base interfaces. A class might include an interface multiple times through base classes that it inherits or through interfaces that other interfaces inherit. However, the class can provide an implementation of an interface only one time and only if the class declares the interface as part of the definition of the class (class ClassName : InterfaceName). If the interface is inherited because you inherited a base class that implements the interface, the base class provides the implementation of the members of the interface. However, the derived class can reimplement any virtual interface members instead of using the inherited implementation. When interfaces declare a default implementation of a method, any class implementing that interface inherits that implementation (You need to cast the class instance to the interface type to access the default implementation on the Interface member).
A base class can also implement interface members by using virtual members. In that case, a derived class can change the interface behavior by overriding the virtual members. For more information about virtual members, see Polymorphism.
Interfaces summary
In C# versions earlier than 8.0, an interface is like an abstract base class with only abstract members. A class or struct that implements the interface must implement all its members.
Beginning with C# 8.0, an interface may define default implementations for some or all of its members. A class or struct that implements the interface doesn't have to implement members that have default implementations.
An interface can't be instantiated directly. Its members are implemented by any class or struct that implements the interface.
A class or struct can implement multiple interfaces. A class can inherit a base class and also implement one or more interfaces.
Introduction to events
Events are, like delegates, a late binding mechanism. In fact, events are built on the language support for delegates.
Events are a way for an object to broadcast (to all interested components in the system) that something has happened. Any other component can subscribe to the event, and be notified when an event is raised.
You've probably used events in some of your programming. Many graphical systems have an event model to report user interaction. These events would report mouse movement, button presses and similar interactions. That's one of the most common, but certainly not the only scenario where events are used.
You can define events that should be raised for your classes. One important consideration when working with events is that there may not be any object registered for a particular event. You must write your code so that it does not raise events when no listeners are configured.
Subscribing to an event also creates a coupling between two objects (the event source, and the event sink). You need to ensure that the event sink unsubscribes from the event source when no longer interested in events.
Language support for events
he syntax for defining events, and subscribing or unsubscribing from events is an extension of the syntax for delegates.
To define an event you use the
event
keyword:
public event EventHandler Progress;
The type of the event (
EventHandler
in this example) must be a delegate type. There are a number of conventions that you should follow when declaring an event. Typically, the event delegate type has a void return. Event declarations should be a verb, or a verb phrase. Use past tense when the event reports something that has happened. Use a present tense verb (for example,
Closing
) to report something that is about to happen. Often, using present tense indicates that your class supports some kind of customization behavior. One of the most common scenarios is to support cancellation. For example, a Closing event may include an argument that would indicate if the close operation should continue, or not. Other scenarios may enable callers to modify behavior by updating properties of the event arguments. You may raise an event to indicate a proposed next action an algorithm will take. The event handler may mandate a different action by modifying properties of the event argument.
When you want to raise the event, you call the event handlers using the delegate invocation syntax:
Progress?.Invoke(this, new FileListArgs(file));
the ?. operator makes it easy to ensure that you do not attempt to raise the event when there are no subscribers to that event.
You subscribe to an event by using the += operator:
The handler method typically has the prefix 'On' followed by the event name, as shown above.
You unsubscribe using the
-=
operator:
fileLister.Progress -= onProgress;
It's important that you declare a local variable for the expression that represents the event handler. That ensures the unsubscribe removes the handler. If, instead, you used the body of the lambda expression, you are attempting to remove a handler that has never been attached, which does nothing.
Properties
Properties are first class citizens in C#. The language defines syntax that enables developers to write code that accurately expresses their design intent.
Properties behave like fields when they're accessed. However, unlike fields, properties are implemented with accessors that define the statements executed when a property is accessed or assigned.
Property syntax
The syntax for properties is a natural extension to fields. A field defines a storage location:
public class Person
{
public string? FirstName;
// Omitted for brevity.
}
A property definition contains declarations for a get and set accessor that retrieves and assigns the value of that property:
public class Person
{
public string? FirstName { get; set; }
// Omitted for brevity.
}
The syntax shown above is the auto property syntax. The compiler generates the storage location for the field that backs up the property. The compiler also implements the body of the get and set accessors.
Sometimes, you need to initialize a property to a value other than the default for its type. C# enables that by setting a value after the closing brace for the property. You may prefer the initial value for the FirstName property to be the empty string rather than null. You would specify that as shown below:
public class Person
{
public string FirstName { get; set; } = string.Empty;
// Omitted for brevity.
}
Specific initialization is most useful for read-only properties, as you'll see later in this article.
You can also define the storage yourself, as shown below:
public class Person
{
public string? FirstName
{
get { return _firstName; }
set { _firstName = value; }
}
private string? _firstName;
// Omitted for brevity.
}
When a property implementation is a single expression, you can use expression-bodied members for the getter or setter:
public class Person
{
public string? FirstName
{
get => _firstName;
set => _firstName = value;
}
private string? _firstName;
// Omitted for brevity.
}
This simplified syntax will be used where applicable throughout this article.
The property definition shown above is a read-write property. Notice the keyword value in the set accessor. The set accessor always has a single parameter named value. The get accessor must return a value that is convertible to the type of the property (string in this example).
That's the basics of the syntax. There are many different variations that support various different design idioms. Let's explore, and learn the syntax options for each.
Validation
The examples above showed one of the simplest cases of property definition: a read-write property with no validation. By writing the code you want in the get and set accessors, you can create many different scenarios.
You can write code in the set accessor to ensure that the values represented by a property are always valid. For example, suppose one rule for the Person class is that the name can't be blank or white space. You would write that as follows:
public class Person
{
public string? FirstName
{
get => _firstName;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("First name must not be blank");
_firstName = value;
}
}
private string? _firstName;
// Omitted for brevity.
}
The preceding example can be simplified by using a throw expression as part of the property setter validation:
public class Person
{
public string? FirstName
{
get => _firstName;
set => _firstName = (!string.IsNullOrWhiteSpace(value)) ? value : throw new ArgumentException("First name must not be blank");
}
private string? _firstName;
// Omitted for brevity.
}
The example above enforces the rule that the first name must not be blank or white space. If a developer writes
hero.FirstName = "";
That assignment throws an ArgumentException. Because a property set accessor must have a void return type, you report errors in the set accessor by throwing an exception.
You can extend this same syntax to anything needed in your scenario. You can check the relationships between different properties, or validate against any external conditions. Any valid C# statements are valid in a property accessor.
Access control
Up to this point, all the property definitions you have seen are read/write properties with public accessors. That's not the only valid accessibility for properties. You can create read-only properties, or give different accessibility to the set and get accessors. Suppose that your Person class should only enable changing the value of the FirstName property from other methods in that class. You could give the set accessor private accessibility instead of public:
public class Person
{
public string? FirstName { get; private set; }
// Omitted for brevity.
}
Now, the FirstName property can be accessed from any code, but it can only be assigned from other code in the Person class.
You can add any restrictive access modifier to either the set or get accessors. Any access modifier you place on the individual accessor must be more limited than the access modifier on the property definition. The above is legal because the FirstName property is public, but the set accessor is private. You couldn't declare a private property with a public accessor. Property declarations can also be declared protected, internal, protected internal, or, even private.
It's also legal to place the more restrictive modifier on the get accessor. For example, you could have a public property, but restrict the get accessor to private. That scenario is rarely done in practice.
Read-only
You can also restrict modifications to a property so that it can only be set in a constructor. You can modify the Person class so as follows:
public class Person
{
public Person(string firstName) => FirstName = firstName;
public string FirstName { get; }
// Omitted for brevity.
}
Init-only
The preceding example requires callers to use the constructor that includes the FirstName parameter. Callers can't use object initializers to assign a value to the property. To support initializers, you can make the set accessor an init accessor, as shown in the following code:
public class Person
{
public Person() { }
public Person(string firstName) => FirstName = firstName;
public string? FirstName { get; init; }
// Omitted for brevity.
}
The preceding example allows a caller to create a Person using the default constructor, even when that code doesn't set the FirstName property. Beginning in C# 11, you can require callers to set that property:
public class Person
{
public Person() { }
[SetsRequiredMembers]
public Person(string firstName) => FirstName = firstName;
public required string FirstName { get; init; }
// Omitted for brevity.
}
The preceding code makes two additions to the Person class. First, the FirstName property declaration includes the required modifier. That means any code that creates a new Person must set this property. Second, the constructor that takes a firstName parameter has the System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute attribute. This attribute informs the compiler that this constructor sets all required members.
Callers must either use the constructor with SetsRequiredMembers or set the FirstName property using an object initializer, as shown in the following code:
var person = new VersionNinePoint2.Person("John");
person = new VersionNinePoint2.Person{ FirstName = "John"};
// Error CS9035: Required member `Person.FirstName` must be set:
//person = new VersionNinePoint2.Person();
Computed properties
A property doesn't need to simply return the value of a member field. You can create properties that return a computed value. Let's expand the Person object to return the full name, computed by concatenating the first and last names:
public class Person
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string FullName { get { return $"{FirstName} {LastName}"; } }
}
The example above uses the string interpolation feature to create the formatted string for the full name.
You can also use an expression-bodied member, which provides a more succinct way to create the computed FullName property:
public class Person
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string FullName => $"{FirstName} {LastName}";
}
Expression-bodied members use the lambda expression syntax to define methods that contain a single expression. Here, that expression returns the full name for the person object.
Cached evaluated properties
You can mix the concept of a computed property with storage and create a cached evaluated property. For example, you could update the FullName property so that the string formatting only happened the first time it was accessed:
public class Person
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
private string? _fullName;
public string FullName
{
get
{
if (_fullName is null)
_fullName = $"{FirstName} {LastName}";
return _fullName;
}
}
}
The above code contains a bug though. If code updates the value of either the FirstName or LastName property, the previously evaluated fullName field is invalid. You modify the set accessors of the FirstName and LastName property so that the fullName field is calculated again:
public class Person
{
private string? _firstName;
public string? FirstName
{
get => _firstName;
set
{
_firstName = value;
_fullName = null;
}
}
private string? _lastName;
public string? LastName
{
get => _lastName;
set
{
_lastName = value;
_fullName = null;
}
}
private string? _fullName;
public string FullName
{
get
{
if (_fullName is null)
_fullName = $"{FirstName} {LastName}";
return _fullName;
}
}
}
This final version evaluates the FullName property only when needed. If the previously calculated version is valid, it's used. If another state change invalidates the previously calculated version, it will be recalculated. Developers that use this class don't need to know the details of the implementation. None of these internal changes affect the use of the Person object. That's the key reason for using Properties to expose data members of an object.
Attaching attributes to auto-implemented properties
Field attributes can be attached to the compiler generated backing field in auto-implemented properties. For example, consider a revision to the Person class that adds a unique integer Id property. You write the Id property using an auto-implemented property, but your design doesn't call for persisting the Id property. The NonSerializedAttribute can only be attached to fields, not properties. You can attach the NonSerializedAttribute to the backing field for the Id property by using the field: specifier on the attribute, as shown in the following example:
public class Person
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
[field:NonSerialized]
public int Id { get; set; }
public string FullName => $"{FirstName} {LastName}";
}
This technique works for any attribute you attach to the backing field on the auto-implemented property.
Implementing INotifyPropertyChanged
A final scenario where you need to write code in a property accessor is to support the INotifyPropertyChanged interface used to notify data binding clients that a value has changed. When the value of a property changes, the object raises the INotifyPropertyChanged.PropertyChanged event to indicate the change. The data binding libraries, in turn, update display elements based on that change. The code below shows how you would implement INotifyPropertyChanged for the FirstName property of this person class.
public class Person : INotifyPropertyChanged
{
public string? FirstName
{
get => _firstName;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("First name must not be blank");
if (value != _firstName)
{
_firstName = value;
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(FirstName)));
}
}
}
private string? _firstName;
public event PropertyChangedEventHandler? PropertyChanged;
}
The
?.
operator is called the null conditional operator. It checks for a null reference before evaluating the right side of the operator. The end result is that if there are no subscribers to the PropertyChanged event, the code to raise the event doesn't execute. It would throw a NullReferenceException without this check in that case. For more information, see events. This example also uses the new nameof operator to convert from the property name symbol to its text representation. Using nameof can reduce errors where you've mistyped the name of the property.
Again, implementing INotifyPropertyChanged is an example of a case where you can write code in your accessors to support the scenarios you need.
Summing up
Properties are a form of smart fields in a class or object. From outside the object, they appear like fields in the object. However, properties can be implemented using the full palette of C# functionality. You can provide validation, different accessibility, lazy evaluation, or any requirements your scenarios need.
Introduction to delegates and events in C#
Delegates provide a late binding mechanism in .NET. Late Binding means that you create an algorithm where the caller also supplies at least one method that implements part of the algorithm.
For example, consider sorting a list of stars in an astronomy application. You may choose to sort those stars by their distance from the earth, or the magnitude of the star, or their perceived brightness.
In all those cases, the Sort() method does essentially the same thing: arranges the items in the list based on some comparison. The code that compares two stars is different for each of the sort orderings.
These kinds of solutions have been used in software for half a century. The C# language delegate concept provides first class language support, and type safety around the concept.
As you'll see later in this series, the C# code you write for algorithms like this is type safe. The compiler ensures that the types match for arguments and return types.
Function pointers support similar scenarios, where you need more control over the calling convention. The code associated with a delegate is invoked using a virtual method added to a delegate type. Using function pointers, you can specify different conventions.
Language Design Goals for Delegates
The language designers enumerated several goals for the feature that eventually became delegates.
The team wanted a common language construct that could be used for any late binding algorithms. Delegates enable developers to learn one concept, and use that same concept across many different software problems.
Second, the team wanted to support both single and multicast method calls. (Multicast delegates are delegates that chain together multiple method calls. You'll see examples later in this series.)
The team wanted delegates to support the same type safety that developers expect from all C# constructs.
Finally, the team recognized an event pattern is one specific pattern where delegates, or any late binding algorithm, is useful. The team wanted to ensure the code for delegates could provide the basis for the .NET event pattern.
The result of all that work was the delegate and event support in C# and .NET.
The remaining articles in this series will cover language features, library support, and common idioms used when you work with delegates and events. You'll learn about:
The delegate keyword and what code it generates.
The features in the System.Delegate class, and how those features are used.
How to create type-safe delegates.
How to create methods that can be invoked through delegates.
How to work with delegates and events by using lambda expressions.
How delegates become one of the building blocks for LINQ.
How delegates are the basis for the .NET event pattern, and how they're different.
System.Delegate and the delegate keyword
Define delegate types
Let's start with the 'delegate' keyword, because that's primarily what you will use as you work with delegates. The code that the compiler generates when you use the delegate keyword will map to method calls that invoke members of the Delegate and MulticastDelegate classes.
You define a delegate type using syntax that is similar to defining a method signature. You just add the delegate keyword to the definition.
Let's continue to use the List.Sort() method as our example. The first step is to create a type for the comparison delegate:
// From the .NET Core library
// Define the delegate type:
public delegate int Comparison(T left, T right);
The compiler generates a class, derived from System.Delegate that matches the signature used (in this case, a method that returns an integer, and has two arguments). The type of that delegate is Comparison. The Comparison delegate type is a generic type. For details on generics see here.
Notice that the syntax may appear as though it is declaring a variable, but it is actually declaring a type. You can define delegate types inside classes, directly inside namespaces, or even in the global namespace.
The compiler also generates add and remove handlers for this new type so that clients of this class can add and remove methods from an instance's invocation list. The compiler will enforce that the signature of the method being added or removed matches the signature used when declaring the method.
Declare instances of delegates
After defining the delegate, you can create an instance of that type. Like all variables in C#, you cannot declare delegate instances directly in a namespace, or in the global namespace.
// inside a class definition:
// Declare an instance of that type:
public Comparison comparator;
The type of the variable is Comparison, the delegate type defined earlier. The name of the variable is comparator.
That code snippet above declared a member variable inside a class. You can also declare delegate variables that are local variables, or arguments to methods.
Invoke delegates
You invoke the methods that are in the invocation list of a delegate by calling that delegate. Inside the Sort() method, the code will call the comparison method to determine which order to place objects:
int result = comparator(left, right);
In the line above, the code invokes the method attached to the delegate. You treat the variable as a method name, and invoke it using normal method call syntax.
That line of code makes an unsafe assumption: There's no guarantee that a target has been added to the delegate. If no targets have been attached, the line above would cause a NullReferenceException to be thrown. The idioms used to address this problem are more complicated than a simple null-check
Assign, add, and remove invocation targets
That's how a delegate type is defined, and how delegate instances are declared and invoked.
Developers that want to use the List.Sort() method need to define a method whose signature matches the delegate type definition, and assign it to the delegate used by the sort method. This assignment adds the method to the invocation list of that delegate object.
Suppose you wanted to sort a list of strings by their length. Your comparison function might be the following:
private static int CompareLength(string left, string right) =>
left.Length.CompareTo(right.Length);
The method is declared as a private method. That's fine. You may not want this method to be part of your public interface. It can still be used as the comparison method when attached to a delegate. The calling code will have this method attached to the target list of the delegate object, and can access it through that delegate.
You create that relationship by passing that method to the List.Sort() method:
phrases.Sort(CompareLength);
Notice that the method name is used, without parentheses. Using the method as an argument tells the compiler to convert the method reference into a reference that can be used as a delegate invocation target, and attach that method as an invocation target.
You could also have been explicit by declaring a variable of type Comparison and doing an assignment:
Using lambda expressions for delegate targets is covered more in a later section.
The Sort() example typically attaches a single target method to the delegate. However, delegate objects do support invocation lists that have multiple target methods attached to a delegate object.
Delegate and MulticastDelegate classes
The language support described above provides the features and support you'll typically need to work with delegates. These features are built on two classes in the .NET Core framework: Delegate and MulticastDelegate.
The System.Delegate class and its single direct subclass, System.MulticastDelegate, provide the framework support for creating delegates, registering methods as delegate targets, and invoking all methods that are registered as a delegate target.
Interestingly, the System.Delegate and System.MulticastDelegate classes are not themselves delegate types. They do provide the basis for all specific delegate types. That same language design process mandated that you cannot declare a class that derives from Delegate or MulticastDelegate. The C# language rules prohibit it.
Instead, the C# compiler creates instances of a class derived from MulticastDelegate when you use the C# language keyword to declare delegate types.
This design has its roots in the first release of C# and .NET. One goal for the design team was to ensure that the language enforced type safety when using delegates. That meant ensuring that delegates were invoked with the right type and number of arguments. And, that any return type was correctly indicated at compile time. Delegates were part of the 1.0 .NET release, which was before generics.
The best way to enforce this type safety was for the compiler to create the concrete delegate classes that represented the method signature being used.
Even though you cannot create derived classes directly, you will use the methods defined on these classes. Let's go through the most common methods that you will use when you work with delegates.
The first, most important fact to remember is that every delegate you work with is derived from MulticastDelegate. A multicast delegate means that more than one method target can be invoked when invoking through a delegate. The original design considered making a distinction between delegates where only one target method could be attached and invoked, and delegates where multiple target methods could be attached and invoked. That distinction proved to be less useful in practice than originally thought. The two different classes were already created, and have been in the framework since its initial public release.
The methods that you will use the most with delegates are Invoke() and BeginInvoke() / EndInvoke(). Invoke() will invoke all the methods that have been attached to a particular delegate instance. As you saw above, you typically invoke delegates using the method call syntax on the delegate variable. As you'll see later in this series, there are patterns that work directly with these methods.
Now that you've seen the language syntax and the classes that support delegates, let's examine how strongly typed delegates are used, created, and invoked.
Strongly Typed Delegates
The abstract Delegate class provides the infrastructure for loose coupling and invocation. Concrete Delegate types become much more useful by embracing and enforcing type safety for the methods that are added to the invocation list for a delegate object. When you use the delegate keyword and define a concrete delegate type, the compiler generates those methods.
In practice, this would lead to creating new delegate types whenever you need a different method signature. This work could get tedious after a time. Every new feature requires new delegate types.
Thankfully, this isn't necessary. The .NET Core framework contains several types that you can reuse whenever you need delegate types. These are generic definitions so you can declare customizations when you need new method declarations.
The first of these types is the Action type, and several variations:
public delegate void Action();
public delegate void Action(T arg);
public delegate void Action(T1 arg1, T2 arg2);
// Other variations removed for brevity.
The in modifier on the generic type argument is covered in the article on covariance.
There are variations of the Action delegate that contain up to 16 arguments such as Action. It's important that these definitions use different generic arguments for each of the delegate arguments: That gives you maximum flexibility. The method arguments need not be, but may be, the same type.
Use one of the Action types for any delegate type that has a void return type.
The framework also includes several generic delegate types that you can use for delegate types that return values:
public delegate TResult Func();
public delegate TResult Func(T1 arg);
public delegate TResult Func(T1 arg1, T2 arg2);
// Other variations removed for brevity
The out modifier on the result generic type argument is covered in the article on covariance.
There are variations of the Func delegate with up to 16 input arguments such as Func. The type of the result is always the last type parameter in all the Func declarations, by convention.
Use one of the Func types for any delegate type that returns a value.
There's also a specialized Predicate type for a delegate that returns a test on a single value:
public delegate bool Predicate(T obj);
You may notice that for any Predicate type, a structurally equivalent Func type exists For example:
You might think these two types are equivalent. They are not. These two variables cannot be used interchangeably. A variable of one type cannot be assigned the other type. The C# type system uses the names of the defined types, not the structure.
All these delegate type definitions in the .NET Core Library should mean that you do not need to define a new delegate type for any new feature you create that requires delegates. These generic definitions should provide all the delegate types you need under most situations. You can simply instantiate one of these types with the required type parameters. In the case of algorithms that can be made generic, these delegates can be used as generic types.
This should save time, and minimize the number of new types that you need to create in order to work with delegates.
Common patterns for delegates
One excellent example for this kind of design is LINQ. The LINQ Query Expression Pattern relies on delegates for all of its features. Consider this simple example:
var smallNumbers = numbers.Where(n => n < 10);
This filters the sequence of numbers to only those less than the value 10. The Where method uses a delegate that determines which elements of a sequence pass the filter. When you create a LINQ query, you supply the implementation of the delegate for this specific purpose.
The prototype for the Where method is:
public static IEnumerable Where (this IEnumerable source, Func predicate);
This example is repeated with all the methods that are part of LINQ. They all rely on delegates for the code that manages the specific query. This API design pattern is a powerful one to learn and understand.
This simple example illustrates how delegates require very little coupling between components. You don't need to create a class that derives from a particular base class. You don't need to implement a specific interface. The only requirement is to provide the implementation of one method that is fundamental to the task at hand.
Build Your Own Components with Delegates
et's build on that example by creating a component using a design that relies on delegates.
Let's define a component that could be used for log messages in a large system. The library components could be used in many different environments, on multiple different platforms. There are a lot of common features in the component that manages the logs. It will need to accept messages from any component in the system. Those messages will have different priorities, which the core component can manage. The messages should have timestamps in their final archived form. For more advanced scenarios, you could filter messages by the source component.
There is one aspect of the feature that will change often: where messages are written. In some environments, they may be written to the error console. In others, a file. Other possibilities include database storage, OS event logs, or other document storage.
There are also combinations of output that might be used in different scenarios. You may want to write messages to the console and to a file.
A design based on delegates will provide a great deal of flexibility, and make it easy to support storage mechanisms that may be added in the future.
Under this design, the primary log component can be a non-virtual, even sealed class. You can plug in any set of delegates to write the messages to different storage media. The built-in support for multicast delegates makes it easy to support scenarios where messages must be written to multiple locations (a file, and a console).
A First Implementation
public static class Logger
{
public static Action? WriteMessage;
public static void LogMessage(string msg)
{
if (WriteMessage is not null)
WriteMessage(msg);
}
}
The static class above is the simplest thing that can work. We need to write the single implementation for the method that writes messages to the console:
public static class LoggingMethods
{
public static void LogToConsole(string message)
{
Console.Error.WriteLine(message);
}
}
Finally, you need to hook up the delegate by attaching it to the WriteMessage delegate declared in the logger:
Our sample so far is fairly simple, but it still demonstrates some of the important guidelines for designs involving delegates.
Using the delegate types defined in the core framework makes it easier for users to work with the delegates. You don't need to define new types, and developers using your library do not need to learn new, specialized delegate types.
The interfaces used are as minimal and as flexible as possible: To create a new output logger, you must create one method. That method may be a static method, or an instance method. It may have any access.
Format Output
Let's make this first version a bit more robust, and then start creating other logging mechanisms.
Next, let's add a few arguments to the LogMessage() method so that your log class creates more structured messages:
public static class Logger
{
public static Action? WriteMessage;
public static void LogMessage(Severity s, string component, string msg)
{
var outputMsg = $"{DateTime.Now}\t{s}\t{component}\t{msg}";
if (WriteMessage is not null)
WriteMessage(outputMsg);
}
}
Next, let's make use of that Severity argument to filter the messages that are sent to the log's output.
public static class Logger
{
public static Action? WriteMessage;
public static Severity LogLevel { get; set; } = Severity.Warning;
public static void LogMessage(Severity s, string component, string msg)
{
if (s < LogLevel)
return;
var outputMsg = $"{DateTime.Now}\t{s}\t{component}\t{msg}";
if (WriteMessage is not null)
WriteMessage(outputMsg);
}
}
Practices
You've added new features to the logging infrastructure. Because the logger component is very loosely coupled to any output mechanism, these new features can be added with no impact on any of the code implementing the logger delegate.
As you keep building this, you'll see more examples of how this loose coupling enables greater flexibility in updating parts of the site without any changes to other locations. In fact, in a larger application, the logger output classes might be in a different assembly, and not even need to be rebuilt.
Build a Second Output Engine
The Log component is coming along well. Let's add one more output engine that logs messages to a file. This will be a slightly more involved output engine. It will be a class that encapsulates the file operations, and ensures that the file is always closed after each write. That ensures that all the data is flushed to disk after each message is generated.
Here is that file-based logger:
public class FileLogger
{
private readonly string logPath;
public FileLogger(string path)
{
logPath = path;
Logger.WriteMessage += LogMessage;
}
public void DetachLog() => Logger.WriteMessage -= LogMessage;
// make sure this can't throw.
private void LogMessage(string msg)
{
try
{
using (var log = File.AppendText(logPath))
{
log.WriteLine(msg);
log.Flush();
}
}
catch (Exception)
{
// Hmm. We caught an exception while
// logging. We can't really log the
// problem (since it's the log that's failing).
// So, while normally, catching an exception
// and doing nothing isn't wise, it's really the
// only reasonable option here.
}
}
}
Once you've created this class, you can instantiate it and it attaches its LogMessage method to the Logger component:
var file = new FileLogger("log.txt");
These two are not mutually exclusive. You could attach both log methods and generate messages to the console and a file:
var fileOutput = new FileLogger("log.txt");
Logger.WriteMessage += LoggingMethods.LogToConsole; // LoggingMethods is the static class we utilized earlier
Later, even in the same application, you can remove one of the delegates without any other issues to the system:
Now, you've added a second output handler for the logging subsystem. This one needs a bit more infrastructure to correctly support the file system. The delegate is an instance method. It's also a private method. There's no need for greater accessibility because the delegate infrastructure can connect the delegates.
Second, the delegate-based design enables multiple output methods without any extra code. You don't need to build any additional infrastructure to support multiple output methods. They simply become another method on the invocation list.
Pay special attention to the code in the file logging output method. It is coded to ensure that it does not throw any exceptions. While this isn't always strictly necessary, it's often a good practice. If either of the delegate methods throws an exception, the remaining delegates that are on the invocation won't be invoked.
As a last note, the file logger must manage its resources by opening and closing the file on each log message. You could choose to keep the file open and implement IDisposable to close the file when you are completed. Either method has its advantages and disadvantages. Both do create a bit more coupling between the classes.
None of the code in the Logger class would need to be updated in order to support either scenario.
Handle Null Delegates
Finally, let's update the LogMessage method so that it is robust for those cases when no output mechanism is selected. The current implementation will throw a NullReferenceException when the WriteMessage delegate does not have an invocation list attached. You may prefer a design that silently continues when no methods have been attached. This is easy using the null conditional operator, combined with the Delegate.Invoke() method:
public static void LogMessage(string msg)
{
WriteMessage?.Invoke(msg);
}
The null conditional operator (?.) short-circuits when the left operand (WriteMessage in this case) is null, which means no attempt is made to log a message.
You won't find the Invoke() method listed in the documentation for System.Delegate or System.MulticastDelegate. The compiler generates a type safe Invoke method for any delegate type declared. In this example, that means Invoke takes a single string argument, and has a void return type.
Summary of Practices
You've seen the beginnings of a log component that could be expanded with other writers, and other features. By using delegates in the design, these different components are loosely coupled. This provides several advantages. It's easy to create new output mechanisms and attach them to the system. These other mechanisms only need one method: the method that writes the log message. It's a design that's resilient when new features are added. The contract required for any writer is to implement one method. That method could be a static or instance method. It could be public, private, or any other legal access.
The Logger class can make any number of enhancements or changes without introducing breaking changes. Like any class, you cannot modify the public API without the risk of breaking changes. But, because the coupling between the logger and any output engines is only through the delegate, no other types (like interfaces or base classes) are involved. The coupling is as small as possible.
C# operators and expressions
C# provides a number of operators. Many of them are supported by the built-in types and allow you to perform basic operations with values of those types. Those operators include the following groups:
Arithmetic operators that perform arithmetic operations with numeric operands
Comparison operators that compare numeric operands
Boolean logical operators that perform logical operations with bool operands
Bitwise and shift operators that perform bitwise or shift operations with operands of the integral types
Equality operators that check if their operands are equal or not
Typically, you can overload those operators, that is, specify the operator behavior for the operands of a user-defined type.
The simplest C# expressions are literals (for example, integer and real numbers) and names of variables. You can combine them into complex expressions by using operators. Operator precedence and associativity determine the order in which the operations in an expression are performed. You can use parentheses to change the order of evaluation imposed by operator precedence and associativity.
In the following code, examples of expressions are at the right-hand side of assignments:
int a, b, c;
a = 7;
b = a;
c = b++;
b = a + b * c;
c = a >= 100 ? b : c / 10;
a = (int)Math.Sqrt(b * b + c * c);
string s = "String literal";
char l = s[s.Length - 1];
List numbers = [..collection];
b = numbers.FindLast(n => n > 1);
Typically, an expression produces a result and can be included in another expression. A void method call is an example of an expression that doesn't produce a result. It can be used only as a statement, as the following example shows:
Console.WriteLine("Hello, world!");
Here are some other kinds of expressions that C# provides:
Interpolated string expressions that provide convenient syntax to create formatted strings:
var r = 2.3;
var message = $"The area of a circle with radius {r} is {Math.PI * r * r:F3}.";
Console.WriteLine(message);
// Output:
// The area of a circle with radius 2.3 is 16.619.
Lambda expressions that allow you to create anonymous functions:
int[] numbers = [2, 3, 4, 5];
var maximumSquare = numbers.Max(x => x * x);
Console.WriteLine(maximumSquare);
// Output:
// 25
Query expressions that allow you to use query capabilities directly in C#:
You can use an expression body definition to provide a concise definition for a method, constructor, property, indexer, or finalizer.
Operator precedence
In an expression with multiple operators, the operators with higher precedence are evaluated before the operators with lower precedence. In the following example, the multiplication is performed first because it has higher precedence than addition:
var a = 2 + 2 * 2;
Console.WriteLine(a); // output: 6
Use parentheses to change the order of evaluation imposed by operator precedence:
var a = (2 + 2) * 2;
Console.WriteLine(a); // output: 8
The following table lists the C# operators starting with the highest precedence to the lowest. The operators within each row have the same precedence.
x = y, x += y, x -= y, x *= y, x /= y, x %= y, x &= y, x |= y, x ^= y, x <<= y, x >>= y, x >>>= y, x ??= y, =>
Assignment and lambda declaration
Operator associativity
When operators have the same precedence, associativity of the operators determines the order in which the operations are performed:
Left-associative operators are evaluated in order from left to right. Except for the assignment operators and the null-coalescing operators, all binary operators are left-associative. For example, a + b - c is evaluated as (a + b) - c.
Right-associative operators are evaluated in order from right to left. The assignment operators, the null-coalescing operators, lambdas, and the conditional operator ?: are right-associative. For example, x = y = z is evaluated as x = (y = z).
int a = 13 / 5 / 2;
int b = 13 / (5 / 2);
Console.WriteLine($"a = {a}, b = {b}"); // output: a = 1, b = 6
Operand evaluation
Unrelated to operator precedence and associativity, operands in an expression are evaluated from left to right. The following examples demonstrate the order in which operators and operands are evaluated:
Expression
Order of evaluation
a + b
a, b, +
a + b * c
a, b, c, *, +
a / b + c * d
a, b, /, c, d, *, +
a / (b + c) * d
a, b, c, +, /, d, *
Typically, all operator operands are evaluated. However, some operators evaluate operands conditionally. That is, the value of the leftmost operand of such an operator defines if (or which) other operands should be evaluated. These operators are the conditional logical AND (&&) and OR (||) operators, the null-coalescing operators ?? and ??=, the null-conditional operators ?. and ?[], and the conditional operator ?:.
Statements
The actions that a program takes are expressed in statements. Common actions include declaring variables, assigning values, calling methods, looping through collections, and branching to one or another block of code, depending on a given condition. The order in which statements are executed in a program is called the flow of control or flow of execution. The flow of control may vary every time that a program is run, depending on how the program reacts to input that it receives at run time.
A statement can consist of a single line of code that ends in a semicolon, or a series of single-line statements in a block. A statement block is enclosed in {} brackets and can contain nested blocks. The following code shows two examples of single-line statements, and a multi-line statement block:
public static void Main()
{
// Declaration statement.
int counter;
// Assignment statement.
counter = 1;
// Error! This is an expression, not an expression statement.
// counter + 1;
// Declaration statements with initializers are functionally
// equivalent to declaration statement followed by assignment statement:
int[] radii = [15, 32, 108, 74, 9]; // Declare and initialize an array.
const double pi = 3.14159; // Declare and initialize constant.
// foreach statement block that contains multiple statements.
foreach (int radius in radii)
{
// Declaration statement with initializer.
double circumference = pi * (2 * radius);
// Expression statement (method invocation). A single-line
// statement can span multiple text lines because line breaks
// are treated as white space, which is ignored by the compiler.
System.Console.WriteLine("Radius of circle #{0} is {1}. Circumference = {2:N2}",
counter, radius, circumference);
// Expression statement (postfix increment).
counter++;
} // End of foreach statement block
} // End of Main method body.
} // End of SimpleStatements class.
Output:
Radius of circle #1 = 15. Circumference = 94.25
Radius of circle #2 = 32. Circumference = 201.06
Radius of circle #3 = 108. Circumference = 678.58
Radius of circle #4 = 74. Circumference = 464.96
Radius of circle #5 = 9. Circumference = 56.55
Types of statements
The following table lists the various types of statements in C# and their associated keywords, with links to topics that include more information:
Category
C# keywords / notes
Declaration statements
A declaration statement introduces a new variable or constant. A variable declaration can optionally assign a value to the variable. In a constant declaration, the assignment is required.
Expression statements
Expression statements that calculate a value must store the value in a variable.
Selection statements
Selection statements enable you to branch to different sections of code, depending on one or more specified conditions. For more information, see the following topics:
if
switch
Iteration statements
Iteration statements enable you to loop through collections like arrays, or perform the same set of statements repeatedly until a specified condition is met. For more information, see the following topics:
do
for
foreach
while
Jump statements
Jump statements transfer control to another section of code. For more information, see the following topics:
break
continue
goto
return
yield
Exception-handling statements
Exception-handling statements enable you to gracefully recover from exceptional conditions that occur at run time. For more information, see the following topics:
throw
try-catch
try-finally
try-catch-finally
checked and unchecked
The checked and unchecked statements enable you to specify whether integral-type numerical operations are allowed to cause an overflow when the result is stored in a variable that is too small to hold the resulting value.
The await statement
If you mark a method with the async modifier, you can use the await operator in the method. When control reaches an await expression in the async method, control returns to the caller, and progress in the method is suspended until the awaited task completes. When the task is complete, execution can resume in the method.For a simple example, see the "Async Methods" section of Methods. For more information, see Asynchronous Programming with async and await.
The yield return statement
An iterator performs a custom iteration over a collection, such as a list or an array. An iterator uses the yield return statement to return each element one at a time. When a yield return statement is reached, the current location in code is remembered. Execution is restarted from that location when the iterator is called the next time.
The fixed statement
The fixed statement prevents the garbage collector from relocating a movable variable. For more information, see fixed.
The lock statement
The lock statement enables you to limit access to blocks of code to only one thread at a time. For more information, see lock.
Labeled statements
You can give a statement a label and then use the goto keyword to jump to the labeled statement. (See the example in the following row.)
The empty statement
The empty statement consists of a single semicolon. It does nothing and can be used in places where a statement is required but no action needs to be performed.
Declaration statements
The following code shows examples of variable declarations with and without an initial assignment, and a constant declaration with the necessary initialization.
Some statements, for example, iteration statements, always have an embedded statement that follows them. This embedded statement may be either a single statement or multiple statements enclosed by {} brackets in a statement block. Even single-line embedded statements can be enclosed in {} brackets, as shown in the following example:
// Recommended style. Embedded statement in block.
foreach (string s in System.IO.Directory.GetDirectories(
System.Environment.CurrentDirectory))
{
System.Console.WriteLine(s);
}
// Not recommended.
foreach (string s in System.IO.Directory.GetDirectories(
System.Environment.CurrentDirectory))
System.Console.WriteLine(s);
An embedded statement that is not enclosed in {} brackets cannot be a declaration statement or a labeled statement. This is shown in the following example:
if(pointB == true)
//Error CS1023:
int radius = 5;
Put the embedded statement in a block to fix the error:
if (b == true)
{
// OK:
System.DateTime d = System.DateTime.Now;
System.Console.WriteLine(d.ToLongDateString());
}
Nested statement blocks
Statement blocks can be nested, as shown in the following code:
foreach (string s in System.IO.Directory.GetDirectories(
System.Environment.CurrentDirectory))
{
if (s.StartsWith("CSharp"))
{
if (s.EndsWith("TempFolder"))
{
return s;
}
}
}
return "Not found.";
Unreachable statements
If the compiler determines that the flow of control can never reach a particular statement under any circumstances, it will produce warning CS0162, as shown in the following example:
// An over-simplified example of unreachable code.
const int val = 5;
if (val < 4)
{
System.Console.WriteLine("I'll never write anything."); //CS0162
}
Attributes
.Attributes provide a powerful method of associating metadata, or declarative information, with code (assemblies, types, methods, properties, and so forth). After an attribute is associated with a program entity, the attribute can be queried at run time by using a technique called reflection.
Attributes have the following properties:
Attributes add metadata to your program. Metadata is information about the types defined in a program. All .NET assemblies contain a specified set of metadata that describes the types and type members defined in the assembly. You can add custom attributes to specify any additional information that is required.
.You can apply one or more attributes to entire assemblies, modules, or smaller program elements such as classes and properties.
.Attributes can accept arguments in the same way as methods and properties.
.Your program can examine its own metadata or the metadata in other programs by using reflection.
Reflection provides objects (of type Type) that describe assemblies, modules, and types. You can use reflection to dynamically create an instance of a type, bind the type to an existing object, or get the type from an existing object and invoke its methods or access its fields and properties. If you're using attributes in your code, reflection enables you to access them. For more information, see Attributes.
Here's a simple example of reflection using the GetType() method - inherited by all types from the Object base class - to obtain the type of a variable:
// Using GetType to obtain type information:
int i = 42;
Type type = i.GetType();
Console.WriteLine(type);
The output is: System.Int32.
The following example uses reflection to obtain the full name of the loaded assembly.
// Using Reflection to get information of an Assembly:
Assembly info = typeof(int).Assembly;
Console.WriteLine(info);
The output is something like: System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e.
Using attributes
Attributes can be placed on almost any declaration, though a specific attribute might restrict the types of declarations on which it's valid. In C#, you specify an attribute by placing the name of the attribute enclosed in square brackets ([]) above the declaration of the entity to which it applies.
In this example, the SerializableAttribute attribute is used to apply a specific characteristic to a class:
[Serializable]
public class SampleClass
{
// Objects of this type can be serialized.
}
A method with the attribute DllImportAttribute is declared like the following example:
Many attributes have parameters, which can be positional, unnamed, or named. Any positional parameters must be specified in a certain order and can't be omitted. Named parameters are optional and can be specified in any order. Positional parameters are specified first. For example, these three attributes are equivalent:
The first parameter, the DLL name, is positional and always comes first; the others are named. In this case, both named parameters default to false, so they can be omitted. Positional parameters correspond to the parameters of the attribute constructor. Named or optional parameters correspond to either properties or fields of the attribute. Refer to the individual attribute's documentation for information on default parameter values.
Attribute targets
The target of an attribute is the entity that the attribute applies to. For example, an attribute may apply to a class, a particular method, or an entire assembly. By default, an attribute applies to the element that follows it. But you can also explicitly identify, for example, whether an attribute is applied to a method, or to its parameter, or to its return value.
To explicitly identify an attribute target, use the following syntax:
[target : attribute-list]
The list of possible target values is shown in the following table.
Target value
Applies to
assembly
Entire assembly
module
Current assembly module
field
Field in a class or a struct
event
Event
method
Method or get and set property accessors
param
Method parameters or set property accessor parameters
property
Property
return
Return value of a method, property indexer, or get property accessor
type
Struct, class, interface, enum, or delegate
You would specify the field target value to apply an attribute to the backing field created for an auto-implemented property.
The following example shows how to apply attributes to assemblies and modules.
using System;
using System.Reflection;
[assembly: AssemblyTitleAttribute("Production assembly 4")]
[module: CLSCompliant(true)]
The following example shows how to apply attributes to methods, method parameters, and method return values in C#.
// default: applies to method
[ValidatedContract]
int Method1() { return 0; }
// applies to method
[method: ValidatedContract]
int Method2() { return 0; }
// applies to parameter
int Method3([ValidatedContract] string contract) { return 0; }
// applies to return value
[return: ValidatedContract]
int Method4() { return 0; }
Common uses for attributes
The following list includes a few of the common uses of attributes in code:
.Marking methods using the WebMethod attribute in Web services to indicate that the method should be callable over the SOAP protocol. For more information, see WebMethodAttribute.
.Describing how to marshal method parameters when interoperating with native code. For more information, see MarshalAsAttribute.
.Describing the COM properties for classes, methods, and interfaces.
.Calling unmanaged code using the DllImportAttribute class.
.Describing your assembly in terms of title, version, description, or trademark.
.Describing which members of a class to serialize for persistence.
.Describing how to map between class members and XML nodes for XML serialization.
.Describing the security requirements for methods.
.Specifying characteristics used to enforce security.
.Controlling optimizations by the just-in-time (JIT) compiler so the code remains easy to debug.
.Obtaining information about the caller to a method.
Reflection overview
Reflection is useful in the following situations:
.When you have to access attributes in your program's metadata.
.For examining and instantiating types in an assembly.
.For building new types at run time. Use classes in System.Reflection.Emit.
.For performing late binding, accessing methods on types created at run time.