You briefly saw in Chapter 1 how to declare a new class called HelloWorld
. In Chapter 2, you learned about the built-in primitive types included with C#. Since you have now also learned about control flow and how to declare methods, it is time to discuss defining your own types. This is the core construct of any C# program; this support for classes and the objects created from them is what makes C# an object-oriented language.
This chapter introduces the basics of object-oriented programming using C#. A key focus is on how to define classes, which are the templates for objects themselves.
All of the constructs of structured programming from the previous chapters still apply within object-oriented programming. However, by wrapping those constructs within classes, you can create larger, more organized programs that are more maintainable. The transition from structured, control-flow-based programs to object-oriented programs revolutionized programming because it provided an extra level of organization. The result was that smaller programs were simplified somewhat. Even more importantly, it was easier to create much larger programs because the code within those programs was better organized.
One of the key advantages of object-oriented programming is that instead of creating new programs entirely from scratch, you can assemble a collection of existing objects from prior work, extending the classes with new features, adding more classes, and thereby providing new functionality.
Readers unfamiliar with object-oriented programming should read the Beginner Topic blocks for an introduction. The general text outside the Beginner Topics focuses on using C# for object-oriented programming with the assumption that readers are already familiar with object-oriented concepts.
This chapter delves into how C# supports encapsulation through its support of constructs such as classes, properties, and access modifiers; we covered methods in the preceding chapter. The next chapter builds on this foundation with the introduction of inheritance and the polymorphism that object-oriented programming enables.
Defining a class involves first specifying the keyword class
, followed by an identifier, as shown in Listing 5.1.
class Employee
{
}
All code that belongs to the class will appear between the curly braces following the class declaration. Although not a requirement, generally you place each class into its own file. This makes it easier to find the code that defines a particular class, because the convention is to name the file using the class name.
Guidelines
DO NOT place more than one class in a single source file.
DO name the source file with the name of the public type it contains.
Once you have defined a new class, you can use that class as though it were built into the framework. In other words, you can declare a variable of that type or define a method that takes a parameter of the new class type. Listing 5.2 demonstrates such declarations.
class Program
{
static void Main()
{
Employee employee1, employee2;
// ...
}
static void IncreaseSalary(Employee employee)
{
// ...
}
}
Now that you have defined a new class type, it is time to instantiate an object of that type. Mimicking its predecessors, C# uses the new
keyword to instantiate an object (see Listing 5.3).
class Program
{
static void Main()
{
Employee employee1 = new Employee();
Employee employee2;
employee2 = new Employee();
IncreaseSalary(employee1);
}
}
Not surprisingly, the assignment can occur in the same statement as the declaration, or in a separate statement.
Unlike the primitive types you have worked with so far, there is no literal way to specify an Employee
. Instead, the new
operator provides an instruction to the runtime to allocate memory for an Employee
object, instantiate the object, and return a reference to the instance.
Although an explicit operator for allocating memory exists, there is no such operator for de-allocating the memory. Instead, the runtime automatically reclaims the memory sometime after the object becomes inaccessible. The garbage collector is responsible for the automatic de-allocation. It determines which objects are no longer referenced by other active objects and then de-allocates the memory for those objects. The result is that there is no compile-time–determined program location where the memory will be collected and restored to the system.
In this trivial example, no explicit data or methods are associated with an Employee
, which renders the object essentially useless. The next section focuses on adding data to an object.
Language Contrast: C++—delete Operator
C# programmers should view the new
operator as a call to instantiate an object, not as a call to allocate memory. Both objects allocated on the heap and objects allocated on the stack support the new
operator, emphasizing the point that new
is not about how memory allocation should take place and whether de-allocation is necessary.
Thus C# does not need the delete
operator found in C++. Memory allocation and de-allocation are details that the runtime manages, allowing the developer to focus more on domain logic. However, though memory is managed by the runtime, the runtime does not manage other resources such as database connections, network ports, and so on. Unlike C++, C# does not support implicit deterministic resource cleanup (the occurrence of implicit object destruction at a compile-time–defined location in the code). Fortunately, C# does support explicit deterministic resource cleanup via a using
statement, and implicit nondeterministic resource cleanup using finalizers.
One of the key aspects of object-oriented design is the grouping of data to provide structure. This section discusses how to add data to the Employee
class. The general object-oriented term for a variable that stores data within a class is member variable. This term is well understood in C#, but the more standard term and the one used in the specification is field, which is a named unit of storage associated with the containing type. Instance fields are variables declared at the class level to store data associated with an object. Hence, association is the relationship between the field data type and the containing field.
In Listing 5.4, the class Employee
has been modified to include three fields: FirstName
, LastName
, and Salary
.
class Employee
{
public string FirstName;
public string LastName;
public string Salary;
}
With these fields added, it is possible to store some fundamental data with every Employee
instance. In this case, you prefix the fields with an access modifier of public
. The use of public
on a field indicates that the data within the field is accessible from classes other than Employee
(see the section “Access Modifiers,” later in this chapter).
As with local variable declarations, a field declaration includes the data type to which the field refers. Furthermore, it is possible to assign fields an initial value at declaration time, as demonstrated with the Salary
field in Listing 5.5.
class Employee
{
public string FirstName;
public string LastName;
public string Salary = "Not enough";
}
We delay the guidelines of naming and coding fields until later in the chapter, after C# properties have been introduced. Suffice it to say, Listing 5.5 does not follow the general convention.
You can set and retrieve the data within fields. However, the fact that a field does not include a static
modifier indicates that it is an instance field. You can access an instance field only from an instance of the containing class (an object). You cannot access it from the class directly (without first creating an instance, in other words).
Listing 5.6 shows an updated look at the Program
class and its utilization of the Employee
class, and Output 5.1 shows the results.
class Program
{
static void Main()
{
Employee employee1 = new Employee();
Employee employee2;
employee2 = new Employee();
employee1.FirstName = "Inigo";
employee1.LastName = "Montoya";
employee1.Salary = "Too Little";
IncreaseSalary(employee1);
Console.WriteLine(
"{0} {1}: {2}",
employee1.FirstName,
employee1.LastName,
employee1.Salary);
// ...
}
static void IncreaseSalary(Employee employee)
{
employee.Salary = "Enough to survive on";
}
}
Inigo Montoya: Enough to survive on
Listing 5.6 instantiates two Employee
objects, as you saw before. Next, it sets each field, calls IncreaseSalary()
to change the salary, and then displays each field associated with the object referenced by employee1
.
Notice that you first have to specify which Employee
instance you are working with. Therefore, the employee1
variable appears as a prefix to the field name when assigning and accessing the field.
One alternative to formatting the names in the WriteLine()
method call within Main()
is to provide a method in the Employee
class that takes care of the formatting. Changing the functionality to be within the Employee
class rather than a member of Program
is consistent with the encapsulation of a class. Why not group the methods relating to the employee’s full name with the class that contains the data that forms the name?
Listing 5.7 demonstrates the creation of such a method.
class Employee
{
public string FirstName;
public string LastName;
public string Salary;
public string GetName()
{
return $"{ FirstName } { LastName }";
}
}
There is nothing particularly special about this method compared to what you learned in Chapter 4, except that now the GetName()
method accesses fields on the object instead of just local variables. In addition, the method declaration is not marked with static
. As you will see later in this chapter, static methods cannot directly access instance fields within a class. Instead, it is necessary to obtain an instance of the class to call any instance member, whether a method or a field.
Given the addition of the GetName()
method, you can update Program.Main()
to use the method, as shown in Listing 5.8 and Output 5.2.
class Program
{
static void Main()
{
Employee employee1 = new Employee();
Employee employee2;
employee2 = new Employee();
employee1.FirstName = "Inigo";
employee1.LastName = "Montoya";
employee1.Salary = "Too Little";
IncreaseSalary(employee1);
Console.WriteLine(
$"{ employee1.GetName() }: { employee1.Salary }");
// ...
}
// ...
}
Inigo Montoya: Enough to survive on
You can obtain the reference to a class from within instance members that belong to the class. To indicate explicitly that the field or method accessed is an instance member of the containing class in C#, you use the keyword this
. Use of this
is implicit when calling any instance member, and it returns an instance of the object itself.
For example, consider the SetName()
method shown in Listing 5.9.
class Employee
{
public string FirstName;
public string LastName;
public string Salary;
public string GetName()
{
return $"{ FirstName } { LastName }";
}
public void SetName(
string newFirstName, string newLastName)
{
this.FirstName = newFirstName;
this.LastName = newLastName;
}
}
This example uses the keyword this
to indicate that the fields FirstName
and LastName
are instance members of the class.
Although the this
keyword can prefix any and all references to local class members, the general guideline is not to clutter code when there is no additional value. Therefore, you should avoid using the this
keyword unless it is required. Listing 5.12 (later in this chapter) is an example of one of the few circumstances when such a requirement exists. Listings 5.9 and 5.10, however, are not good examples. In Listing 5.9, this
can be dropped entirely without changing the meaning of the code. And in Listing 5.10 (presented next), by changing the naming convention for fields, we can avoid any ambiguity between local variables and fields.
Language Contrast: Visual Basic—Accessing a Class Instance with Me
The C# keyword this
is identical to the Visual Basic keyword Me
.
In Listing 5.9 and Listing 5.10, the this
keyword is not used in the GetName()
method—it is optional. However, if local variables or parameters exist with the same name as the field (see the SetName()
method in Listing 5.10), omitting this
would result in accessing the local variable/parameter when the intention was the field; given this scenario, use of this
is required.
You also can use the keyword this
to access a class’s methods explicitly. For example, this.GetName()
is allowed within the SetName()
method, permitting you to print out the newly assigned name (see Listing 5.11 and Output 5.3).
class Employee
{
// ...
public string GetName()
{
return $"{ FirstName } { LastName }";
}
public void SetName(string newFirstName, string newLastName)
{
this.FirstName = newFirstName;
this.LastName = newLastName;
Console.WriteLine(
$"Name changed to '{ this.GetName() }'");
}
}
class Program
{
static void Main()
{
Employee employee = new Employee();
employee.SetName("Inigo", "Montoya");
// ...
}
// ...
}
Name changed to 'Inigo Montoya'
Sometimes it may be necessary to use this
to pass a reference to the currently executing object. Consider the Save()
method in Listing 5.12.
class Employee
{
public string FirstName;
public string LastName;
public string Salary;
public void Save()
{
DataStorage.Store(this);
}
}
class DataStorage
{
// Save an employee object to a file
// named with the Employee name.
public static void Store(Employee employee)
{
// ...
}
}
The Save()
method in Listing 5.12 calls a method on the DataStorage
class, called Store()
. The Store()
method, however, needs to be passed the Employee
object, which needs to be persisted. This is done using the keyword this
, which passes the instance of the Employee
object on which Save()
was called.
When declaring a field earlier in the chapter, you prefixed the field declaration with the keyword public
. public
is an access modifier that identifies the level of encapsulation associated with the member it decorates. Five access modifiers are available: public
, private
, protected
, internal
, and protected internal
. This section considers the first two.
The purpose of an access modifier is to provide encapsulation. By using public
, you explicitly indicate that it is acceptable that the modified fields are accessible from outside the Employee
class—in other words, that they are accessible from the Program
class, for example.
Consider an Employee
class that includes a Password
field, however. It should be possible to call an Employee
object and verify the password using a Logon()
method. Conversely, it should not be possible to access the Password
field on an Employee
object from outside the class.
To define a Password
field as hidden and inaccessible from outside the containing class, you use the keyword private
for the access modifier, in place of public
(see Listing 5.15). As a result, the Password
field is not accessible from inside the Program
class, for example.
class Employee
{
public string FirstName;
public string LastName;
public string Salary;
private string Password;
private bool IsAuthenticated;
public bool Logon(string password)
{
if(Password == password)
{
IsAuthenticated = true;
}
return IsAuthenticated;
}
public bool GetIsAuthenticated()
{
return IsAuthenticated;
}
// ...
}
class Program
{
static void Main()
{
Employee employee = new Employee();
employee.FirstName = "Inigo";
employee.LastName = "Montoya";
// ...
// Password is private, so it cannot be
// accessed from outside the class.
// Console.WriteLine(
// ("Password = {0}", employee.Password);
}
// ...
}
Although this option is not shown in Listing 5.15, it is possible to decorate a method with an access modifier of private
as well.
If no access modifier is placed on a class member, the declaration defaults to private
. In other words, members are private by default and programmers need to specify explicitly that a member is to be public.
The preceding section, “Access Modifiers,” demonstrated how you can use the private
keyword to encapsulate a password, preventing access from outside the class. This type of encapsulation is often too strict, however. For example, sometimes you might need to define fields that external classes can only read, but whose values you can change internally. Alternatively, perhaps you want to allow access to write some data in a class, but you need to be able to validate changes made to the data. In yet another scenario, perhaps you need to construct the data on the fly. Traditionally, languages enabled the features found in these examples by marking fields as private and then providing getter and setter methods for accessing and modifying the data. The code in Listing 5.16 changes both FirstName
and LastName
to private fields. Public getter and setter methods for each field allow their values to be accessed and changed.
class Employee
{
private string FirstName;
// FirstName getter
public string GetFirstName()
{
return FirstName;
}
// FirstName setter
public void SetFirstName(string newFirstName)
{
if(newFirstName != null && newFirstName != "")
{
FirstName = newFirstName;
}
}
private string LastName;
// LastName getter
public string GetLastName()
{
return LastName;
}
// LastName setter
public void SetLastName(string newLastName)
{
if(newLastName != null && newLastName != "")
{
LastName = newLastName;
}
}
// ...
}
Unfortunately, this change affects the programmability of the Employee
class. No longer can you use the assignment operator to set data within the class, nor can you access the data without calling a method.
Recognizing the frequency of this type of pattern, the C# designers provided explicit syntax for it. This syntax is called a property (see Listing 5.17 and Output 5.5).
class Program
{
static void Main()
{
Employee employee = new Employee();
// Call the FirstName property's setter.
employee.FirstName = "Inigo";
// Call the FirstName property's getter.
System.Console.WriteLine(employee.FirstName);
}
}
class Employee
{
// FirstName property
public string FirstName
{
get
{
return _FirstName;
}
set
{
_FirstName = value;
}
}
private string _FirstName;
// LastName property
public string LastName
{
get
{
return _LastName;
}
set
{
_LastName = value;
}
}
private string _LastName;
// ...
}
Inigo
The first thing to notice in Listing 5.17 is not the property code itself, but rather the code within the Program
class. Although you no longer have the fields with the FirstName
and LastName
identifiers, you cannot see this by looking at the Program
class. The API for accessing an employee’s first and last names has not changed at all. It is still possible to assign the parts of the name using a simple assignment operator, for example (employee.FirstName = "Inigo"
).
The key feature is that properties provide an API that looks programmatically like a field. In actuality, no such fields exist. A property declaration looks exactly like a field declaration, but following it are curly braces in which to place the property implementation. Two optional parts make up the property implementation. The get
part defines the getter portion of the property. It corresponds directly to the GetFirstName()
and GetLastName()
functions defined in Listing 5.16. To access the FirstName
property, you call employee.FirstName
. Similarly, setters (the set
portion of the implementation) enable the calling syntax of the field assignment:
employee.FirstName = "Inigo";
Property definition syntax uses three contextual keywords. You use the get
and set
keywords to identify either the retrieval or the assignment portion of the property, respectively. In addition, the setter uses the value
keyword to refer to the right side of the assignment operation. When Program.Main()
calls employee.FirstName = "Inigo"
, therefore, value
is set to "Inigo"
inside the setter and can be used to assign _FirstName
. Listing 5.17’s property implementations are the most commonly used. When the getter is called (such as in Console.WriteLine(employee.FirstName)
), the value from the field (_FirstName
) is obtained and written to the console.
Begin 3.0
In C# 3.0, property syntax includes a shorthand version. Since a property with a single backing field that is assigned and retrieved by the get and set accessors is so trivial and common (see the implementations of FirstName
and LastName
), the C# 3.0 compiler (and higher) allows the declaration of a property without any accessor implementation or backing field declaration. Listing 5.18 demonstrates the syntax with the Title
and Manager
properties, and Output 5.6 shows the results.
class Program
{
static void Main()
{
Employee employee1 =
new Employee();
Employee employee2 =
new Employee();
// Call the FirstName property's setter.
employee1.FirstName = "Inigo";
// Call the FirstName property's getter.
System.Console.WriteLine(employee1.FirstName);
// Assign an auto-implemented property
employee2.Title = "Computer Nerd";
employee1.Manager = employee2;
// Print employee1's manager's title.
System.Console.WriteLine(employee1.Manager.Title);
}
}
class Employee
{
// FirstName property
public string FirstName
{
get
{
return _FirstName;
}
set
{
_FirstName = value;
}
}
private string _FirstName;
// LastName property
public string LastName
{
get
{
return _LastName;
}
set
{
_LastName = value;
}
}
private string _LastName;
public string Title { get; set; }
public Employee Manager { get; set; }
public string Salary { get; set; } = "Not Enough";
// ...
}
Inigo
Computer Nerd
Auto-implemented properties provide for a simpler way of writing properties in addition to reading them. Furthermore, when it comes time to add something such as validation to the setter, any existing code that calls the property will not have to change, even though the property declaration will have changed to include an implementation.
Throughout the remainder of the book, we will frequently use this C# 3.0 or later syntax without indicating that it is a feature introduced in C# 3.0.
End 3.0
Begin 6.0
One final thing to note about automatically declared properties is that in C# 6.0, it is possible to initialize them as Listing 5.18 does for Salary
:
public string Salary { get; set; } = "Not Enough";
Prior to C# 6.0, property initialization was possible only via a method (including the constructor, as we discuss later in the chapter). However, with C# 6.0, you can initialize automatically implemented properties at declaration time using a syntax much like that used for field initialization.
End 6.0
Given that it is possible to write explicit setter and getter methods rather than properties, on occasion a question may arise as to whether it is better to use a property or a method. The general guideline is that methods should represent actions and properties should represent data. Properties are intended to provide simple access to simple data with a simple computation. The expectation is that invoking a property will not be significantly more expensive than accessing a field.
With regard to naming, notice that in Listing 5.18 the property name is FirstName
, and the field name changed from earlier listings to _FirstName
—that is, PascalCase with an underscore suffix. Other common naming conventions for the private field that backs a property are _firstName
and m_FirstName
(a holdover from C++, where the m stands for member variable), and on occasion the camelCase convention, just like with local variables.3 The camelCase convention should be avoided, however. The camelCase used for property names is the same as the naming convention used for local variables and parameters, meaning that overlaps in names become highly probable. Also, to respect the principles of encapsulation, fields should not be declared as public or protected.
3. We prefer _FirstName
because the m in front of the name is unnecessary when compared with an underscore (_
). Also, by using the same casing as the property, it is possible to have only one string within the Visual Studio code template expansion tools, instead of having one for both the property name and the field name.
Guidelines
DO use properties for simple access to simple data with simple computations.
AVOID throwing exceptions from property getters.
DO preserve the original property value if the property throws an exception.
DO favor automatically implemented properties over properties with simple backing fields when no additional logic is required.
Regardless of which naming pattern you use for private fields, the coding standard for properties is PascalCase. Therefore, properties should use the LastName
and FirstName
pattern with names that represent nouns, noun phrases, or adjectives. It is not uncommon, in fact, that the property name is the same as the type name. Consider an Address
property of type Address
on a Person
object, for example.
CONSIDER using the same casing on a property’s backing field as that used in the property, distinguishing the backing field with an “_” prefix. Do not, however, use two underscores; identifiers beginning with two underscores are reserved for the use of the C# compiler itself.
DO name properties using a noun, noun phrase, or adjective.
CONSIDER giving a property the same name as its type.
AVOID naming fields with camelCase.
DO favor prefixing Boolean properties with “Is,” “Can,” or “Has,” when that practice adds value.
DO NOT declare instance fields that are public or protected. (Instead, expose them via a property.)
DO name properties with PascalCase.
DO favor automatically implemented properties over fields.
DO favor automatically implemented properties over using fully expanded ones if there is no additional implementation logic.
Notice in Listing 5.19 that the Initialize()
method of Employee
uses the property rather than the field for assignment as well. Although this is not required, the result is that any validation within the property setter will be invoked both inside and outside the class. Consider, for example, what would happen if you changed the LastName
property so that it checked value
for null
or an empty string, before assigning it to _LastName
.
class Employee
{
// ...
public void Initialize(
string newFirstName, string newLastName)
{
// Use property inside the Employee
// class as well.
FirstName = newFirstName;
LastName = newLastName;
}
// LastName property
public string LastName
{
get
{
return _LastName;
}
set
{
// Validate LastName assignment
if(value == null)
{
// Report error
// In C# 6.0 replace "value" with nameof(value)
throw new ArgumentNullException("value");
}
else
{
// Remove any whitespace around
// the new last name.
value = value.Trim();
if(value == "")
{
// Report error
// In C# 6.0 replace "value" with nameof(value)
throw new ArgumentException(
"LastName cannot be blank.", "value");4
}
else
_LastName = value;
}
}
}
private string _LastName;
// ...
}
4. Apologies to Teller, Cher, Sting, Madonna, Bono, Prince, Liberace, et al.
With this new implementation, the code throws an exception if LastName
is assigned an invalid value, either from another member of the same class or via a direct assignment to LastName
from inside Program.Main()
. The ability to intercept an assignment and validate the parameters by providing a field-like API is one of the advantages of properties.
It is a good practice to access a property-backing field only from inside the property implementation. In other words, you should always use the property, rather than calling the field directly. In many cases, this principle holds even from code within the same class as the property. If you follow this practice, when you add code such as validation code, the entire class immediately takes advantage of it.5
5. As described later in the chapter, one exception to this occurs when the field is marked as read-only, because then the value can be set only in the constructor. In C# 6.0, you can directly assign the value of a read-only property, completely eliminating the need for the read-only field.
Although rare, it is possible to assign value
inside the setter, as Listing 5.19 does. In this case, the call to value.Trim()
removes any whitespace surrounding the new last name value.
Guidelines
AVOID accessing the backing field of a property outside the property, even from within the containing class.
DO use “value” for the paramName
argument when calling the ArgumentException()
or ArgumentNullException()
constructor (“value” is the implicit name of the parameter on property setters).
Begin 6.0
End 6.0
By removing either the getter or the setter portion of a property, you can change a property’s accessibility. Properties with only a setter are write-only, which is a relatively rare occurrence. Similarly, providing only a getter will cause the property to be read-only; any attempts to assign a value will cause a compile error. To make Id
read-only, for example, you would code it as shown in Listing 5.20.
class Program
{
static void Main()
{
Employee employee1 = new Employee();
employee1.Initialize(42);
// ERROR: Property or indexer 'Employee.Id'
// cannot be assigned to; it is read-only.
// employee1.Id = "490";
}
}
class Employee
{
public void Initialize(int id)
{
// Use field because Id property has no setter;
// it is read-only.
_Id = id.ToString();
}
// ...
// Id property declaration
public string Id
{
get
{
return _Id;
}
// No setter provided.
}
private string _Id;
}
Listing 5.20 assigns the field from within the Employee
constructor rather than the property (_Id = id
). Assigning via the property causes a compile error, as it does in Program.Main()
.
Begin 6.0
Starting in C# 6.0, there is also support for read-only automatically implemented properties as follows:
public bool[,,] Cells { get; } = new bool[2, 3, 3];
This is clearly a significant improvement over the pre-C# 6.0 approach, especially given the commonality of read-only properties for something like an array of items or the Id
in Listing 5.20.
One important note about a read-only automatically implemented property is that, like read-only fields, the compiler requires that it be initialized in the constructor or via an initializer. In the preceding snippet we use an initializer, but the assignment of Cells
from within the constructor is also permitted.
Given the guideline that fields should not be accessed from outside their wrapping property, those programming in a C# 6.0 world will discover that there is virtually no need to ever use pre-C# 6.0 syntax; instead, the programmer can always use a read-only, automatically implemented property. The only exception might be when the data type of the read-only modified field does not match the data type of the property—for example, if the field was of type int
and the read-only property was of type double
.
Guidelines
DO create read-only properties if the property value should not be changed.
DO create read-only automatically implemented properties in C# 6.0 (or later) rather than read-only properties with a backing field if the property value should not be changed
End 6.0
As you have seen, properties behave like virtual fields. In some instances, you do not need a backing field at all. Instead, the property getter returns a calculated value while the setter parses the value and persists it to some other member fields (if it even exists). Consider, for example, the Name
property implementation shown in Listing 5.21. Output 5.7 shows the results.
class Program
{
static void Main()
{
Employee employee1 = new Employee();
employee1.Name = "Inigo Montoya";
System.Console.WriteLine(employee1.Name);
// ...
}
}
class Employee
{
// ...
// FirstName property
public string FirstName
{
get
{
return _FirstName;
}
set
{
_FirstName = value;
}
}
private string _FirstName;
// LastName property
public string LastName
{
get
{
return _LastName;
}
set
{
_LastName = value;
}
}
private string _LastName;
// ...
// Name property
public string Name
{
get
{
return $"{ FirstName } { LastName }";
}
set
{
// Split the assigned value into
// first and last names.
string[] names;
names = value.Split(new char[]{' '});
if(names.Length == 2)
{
FirstName = names[0];
LastName = names[1];
}
else
{
// Throw an exception if the full
// name was not assigned.
throw new System. ArgumentException (
$"Assigned value '{ value }' is invalid", "value");
}
}
}
public string Initials => $"{ FirstName[0] } { LastName[0] }";
// ...
}
Inigo Montoya
The getter for the Name
property concatenates the values returned from the FirstName
and LastName
properties. In fact, the name value assigned is not actually stored. When the Name
property is assigned, the value on the right side is parsed into its first and last name parts.
As previously mentioned, it is a good practice not to access fields from outside their properties because doing so circumvents any validation or additional logic that may be inserted. Unfortunately, C# 1.0 did not allow different levels of encapsulation between the getter and setter portions of a property. It was not possible, therefore, to create a public getter and a private setter so that external classes would have read-only access to the property while code within the class could write to the property.
In C# 2.0, support was added for placing an access modifier on either the get or the set portion of the property implementation (not on both), thereby overriding the access modifier specified on the property declaration. Listing 5.22 demonstrates how to do this.
class Program
{
static void Main()
{
Employee employee1 = new Employee();
employee1.Initialize(42);
// ERROR: The property or indexer 'Employee.Id'
// cannot be used in this context because the set
// accessor is inaccessible
employee1.Id = "490";
}
}
class Employee
{
public void Initialize(int id)
{
// Set Id property
Id = id.ToString();
}
// ...
// Id property declaration
public string Id
{
get
{
return _Id;
}
// Providing an access modifier is possible in C# 2.0
// and higher only
private set
{
_Id = value;
}
}
private string _Id;
}
By using private
on the setter, the property appears as read-only to classes other than Employee
. From within Employee
, the property appears as read/write, so you can assign the property within the constructor. When specifying an access modifier on the getter or setter, take care that the access modifier is more restrictive than the access modifier on the property as a whole. It is a compile error, for example, to declare the property as private
and the setter as public
.
Guidelines
DO apply appropriate accessibility modifiers on implementations of getters and setters on all properties.
DO NOT provide set-only properties or properties with the setter having broader accessibility than the getter.
End 2.0
C# allows properties to be used identically to fields, except when they are passed as ref
or out
parameter values. ref
and out
parameter values are internally implemented by passing the memory address to the target method. However, because properties can be virtual fields that have no backing field, or can be read-only or write-only, it is not possible to pass the address for the underlying storage. As a result, you cannot pass properties as ref
or out
parameter values. The same is true for method calls. Instead, when code needs to pass a property or method call as a ref
or out
parameter value, the code must first copy the value into a variable and then pass the variable. Once the method call has completed, the code must assign the variable back into the property.