Читать книгу Professional C# 6 and .NET Core 1.0 - Christian Nagel - Страница 11
PART I
The C# Language
3
Objects and Types
ОглавлениеWhat’s In This Chapter?
• The differences between classes and structs
• Class members
• Expression-bodied members
• Passing values by value and by reference
• Method overloading
• Constructors and static constructors
• Read-only fields
• Enumerations
• Partial classes
• Static classes
• The Object class, from which all other types are derived
Wrox.com Code Downloads for This Chapter
The wrox.com code downloads for this chapter are found at www.wrox.com/go/professionalcsharp6 on the Download Code tab. The code for this chapter is divided into the following major examples:
• MathSample
• MethodSample
• StaticConstructorSample
• StructsSample
• PassingByValueAndByReference
• OutKeywordSample
• EnumSample
• ExtensionMethods
Creating and Using Classes
So far, you’ve been introduced to some of the building blocks of the C# language, including variables, data types, and program flow statements, and you have seen a few very short complete programs containing little more than the Main method. What you haven’t seen yet is how to put all these elements together to form a longer, complete program. The key to this lies in working with classes – the subject of this chapter. Note Chapter 4, “Inheritance,” covers inheritance and features related to inheritance.
NOTE This chapter introduces the basic syntax associated with classes. However, we assume that you are already familiar with the underlying principles of using classes – for example, that you know what a constructor or a property is. This chapter is largely confined to applying those principles in C# code.
Classes and Structs
Classes and structs are essentially templates from which you can create objects. Each object contains data and has methods to manipulate and access that data. The class defines what data and behavior each particular object (called an instance) of that class can contain. For example, if you have a class that represents a customer, it might define fields such as CustomerID, FirstName, LastName, and Address, which are used to hold information about a particular customer. It might also define functionality that acts upon the data stored in these fields. You can then instantiate an object of this class to represent one specific customer, set the field values for that instance, and use its functionality:
Structs differ from classes because they do not need to be allocated on the heap (classes are reference types and are always allocated on the heap). Structs are value types and are usually stored on the stack. Also, structs cannot derive from a base struct.
You typically use structs for smaller data types for performance reasons. In terms of syntax, however, structs look very similar to classes; the main difference is that you use the keyword struct instead of class to declare them. For example, if you wanted all PhoneCustomer instances to be allocated on the stack instead of the managed heap, you could write the following:
For both classes and structs, you use the keyword new to declare an instance. This keyword creates the object and initializes it; in the following example, the default behavior is to zero out its fields:
In most cases, you use classes much more often than structs. Therefore, this chapter covers classes first and then the differences between classes and structs and the specific reasons why you might choose to use a struct instead of a class. Unless otherwise stated, however, you can assume that code presented for a class works equally well for a struct.
NOTE An important difference between classes and structs is that objects of type of class are passed by reference, and objects of type of a struct are passed by value. This is explained later in this chapter in the section “Passing Parameters by Value and by Reference.”
Classes
A class contains members, which can be static or instance members. A static member belongs to the class; an instance member belongs to the object. With static fields, the value of the field is the same for every object. With instance fields, every object can have a different value. Static members have the static modifier attached.
The kind of members are explained in the following table.
Let’s get into the details of class members.
Fields
Fields are any variables associated with the class. You have already seen fields in use in the PhoneCustomer class in the previous example.
After you have instantiated a PhoneCustomer object, you can then access these fields using the object.FieldName syntax, as shown in this example:
Constants can be associated with classes in the same way as variables. You declare a constant using the const keyword. If it is declared as public, then it is accessible from outside the class:
It’s a good idea not to declare fields public. If you change a public member of a class, every caller that’s using this public member needs to be changed as well. For example, in case you want to introduce a check for the maximum string length with the next version, the public field needs to be changed to a property. Existing code that makes use of the public field must be recompiled for using this property (although the syntax from the caller side looks the same with properties). If instead you just change the check within an existing property, the caller doesn’t need to be recompiled for using the new version.
It’s good practice to declare fields private and use properties to access the field, as described in the next section.
Properties
The idea of a property is that it is a method or a pair of methods dressed to look like a field. Let’s change the field for the first name from the previous example to a private field with the variable name _firstName. The property named FirstName contains a get and set accessor to retrieve and set the value of the backing field:
The get accessor takes no parameters and must return the same type as the declared property. You should not specify any explicit parameters for the set accessor either, but the compiler assumes it takes one parameter, which is of the same type again, and which is referred to as value.
Let’s get into another example with a different naming convention. The following code contains a property called Age, which sets a field called age. In this example, age is referred to as the backing variable for the property Age:
Note the naming convention used here. You take advantage of C#’s case sensitivity by using the same name – Pascal-case for the public property, and camel-case for the equivalent private field if there is one. In earlier .NET versions, this naming convention was preferred by Microsoft’s C# team. Recently they switched to the naming convention to prefix field names by an underscore. This provides an extremely convenient way to identify fields in contrast to local variables.
NOTE Microsoft teams use either one or the other naming convention. For using private members of types,NET doesn’t have strict naming conventions. However, within a team the same convention should be used. The .NET Core team switched to using an underscore to prefix fields, which is the convention used in this book in most places (see https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/coding-style.md).
Auto-Implemented Properties
If there isn’t going to be any logic in the properties set and get, then auto-implemented properties can be used. Auto-implemented properties implement the backing member variable automatically. The code for the earlier Age example would look like this:
The declaration of a private field is not needed. The compiler creates this automatically. With auto-implemented properties, you cannot access the field directly as you don’t know the name the compiler generates.
By using auto-implemented properties, validation of the property cannot be done at the property set. Therefore, with the Age property you could not have checked to see if an invalid age is set.
Auto-implemented properties can be initialized using a property initializer:
Access Modifiers for Properties
C# allows the set and get accessors to have differing access modifiers. This would allow a property to have a public get and a private or protected set. This can help control how or when a property can be set. In the following code example, notice that the set has a private access modifier but the get does not. In this case, the get takes the access level of the property. One of the accessors must follow the access level of the property. A compile error is generated if the get accessor has the protected access level associated with it because that would make both accessors have a different access level from the property.
Different access levels can also be set with auto-implemented properties:
NOTE You can also define properties that only have a get or set accessor. Before creating a property with only a set accessor, it’s a good practice to create a method instead. You can use properties with only a get accessor for read-only access. Auto-implemented properties with only get accessors are new with C# 6 and discussed in the section “Readonly Members.”
NOTE Some developers may be concerned that the previous sections have presented a number of situations in which standard C# coding practices have led to very small functions – for example, accessing a field via a property instead of directly. Will this hurt performance because of the overhead of the extra function call? The answer is no. There’s no need to worry about performance loss from these kinds of programming methodologies in C#. Recall that C# code is compiled to IL, then JIT compiled at runtime to native executable code. The JIT compiler is designed to generate highly optimized code and will ruthlessly inline code as appropriate (in other words, it replaces function calls with inline code). A method or property whose implementation simply calls another method or returns a field will almost certainly be inlined.
Usually you do not need to change the inlining behavior, but you have some control to inform the compiler about inlining. Using the attribute MethodImpl, you can define that a method should not be inlined (MethodImplOptions.NoInlining), or inlining should be done aggressively by the compiler (MethodImplOptions.AggressiveInlining). With properties, you need to apply this attribute directly to the get and set accessors. Attributes are explained in detail in Chapter 16, “Reflection, Metadata, and Dynamic Programming.”
Methods
Note that official C# terminology makes a distinction between functions and methods. In C# terminology, the term “function member” includes not only methods, but also other nondata members of a class or struct. This includes indexers, operators, constructors, destructors, and – perhaps somewhat surprisingly – properties. These are contrasted with data members: fields, constants, and events.
Declaring Methods
In C#, the definition of a method consists of any method modifiers (such as the method’s accessibility), followed by the type of the return value, followed by the name of the method, followed by a list of input arguments enclosed in parentheses, followed by the body of the method enclosed in curly braces:
Each parameter consists of the name of the type of the parameter, and the name by which it can be referenced in the body of the method. Also, if the method returns a value, a return statement must be used with the return value to indicate each exit point, as shown in this example:
If the method doesn’t return anything, specify a return type of void because you can’t omit the return type altogether; and if it takes no arguments, you still need to include an empty set of parentheses after the method name. In this case, including a return statement is optional – the method returns automatically when the closing curly brace is reached.
Expression-Bodied Methods
If the implementation of a method consists just of one statement, C# 6 gives a simplified syntax to method definitions: expression-bodied methods. You don’t need to write curly brackets and the return keyword with the new syntax. The operator => (the lambda operator) is used to distinguish the declaration of the left side of this operator to the implementation that is on the right side.
The following example is the same method as before, IsSquare, implemented using the expression-bodied method syntax. The right side of the lambda operator defines the implementation of the method. Curly brackets and a return statement are not needed. What’s returned is the result of the statement, and the result needs to be of the same type as the method declared on the left side, which is a bool in this code snippet:
Invoking Methods
The following example illustrates the syntax for definition and instantiation of classes, and definition and invocation of methods. The class Math defines instance and static members (code file MathSample/Math.cs):
The Program class makes use of the Math class, calls static methods, and instantiates an object to invoke instance members (code file MathSample/Program.cs);
Running the MathSample example produces the following results:
As you can see from the code, the Math class contains a property that contains a number, as well as a method to find the square of this number. It also contains two static methods: one to return the value of pi and one to find the square of the number passed in as a parameter.
Some features of this class are not really good examples of C# program design. For example, GetPi would usually be implemented as a const field, but following good design would mean using some concepts that have not yet been introduced.
Method Overloading
C# supports method overloading – several versions of the method that have different signatures (that is, the same name but a different number of parameters and/or different parameter data types). To overload methods, simply declare the methods with the same name but different numbers of parameter types:
It’s not just the parameter types that can differ; the number of parameters can differ too, as shown in the next example. One overloaded method can invoke another:
NOTE With method overloading, it is not sufficient to only differ overloads by the return type. It’s also not sufficient to differ by parameter names. The number of parameters and/or types needs to difffer.
Named Arguments
Invoking methods, the variable name need not be added to the invocation. However, if you have a method signature like the following to move a rectangle
and you invoke it with the following code snippet, it’s not clear from the invocation what numbers are used for what:
You can change the invocation to make it immediately clear what the numbers mean:
Any method can be invoked using named arguments. You just need to write the name of the variable followed by a colon and the value passed. The compiler gets rid of the name and creates an invocation of the method just like the variable name would not be there – so there’s no difference within the compiled code.
You can also change the order of variables this way, and the compiler rearranges it to the correct order. The real advantage to this is shown in the next section with optional arguments.
Optional Arguments
Parameters can also be optional. You must supply a default value for optional parameters, which must be the last ones defined:
This method can now be invoked using one or two parameters. Passing one parameter, the compiler changes the method call to pass 42 with the second parameter.
NOTE Because the compiler changes methods with optional parameters to pass the default value, the default value should never change with newer versions of the assembly. With a change of the default value in a newer version, if the caller is in a different assembly that is not recompiled, it would have the older default value. That’s why you should have optional parameters only with values that never change. In case the calling method is always recompiled when the default value changes, this is not an issue.
You can define multiple optional parameters, as shown here:
This way, the method can be called using 1, 2, 3, or 4 parameters. The first line of the following code leaves the optional parameters with the values 11, 22, and 33. The second line passes the first three parameters, and the last one has a value of 33:
With multiple optional parameters, the feature of named arguments shines. Using named arguments you can pass any of the optional parameters – for example, this example passes just the last one:
NOTE Pay attention to versioning issues when using optional arguments. One issue is to change default values in newer versions; another issue is to change the number of arguments. It might look tempting to add another optional parameter as it is optional anyway. However, the compiler changes the calling code to fill in all the parameters, and that’s the reason earlier compiled callers fail if another parameter is added later on.
Variable Number of Arguments
Using optional arguments, you can define a variable number of arguments. However, there’s also a different syntax that allows passing a variable number of arguments – and this syntax doesn’t have versioning issues.
Declaring the parameter of type array – the sample code uses an int array – and adding the params keyword, the method can be invoked using any number of int parameters.
NOTE Arrays are explained in detail in Chapter 7, “Arrays and Tuples.”
As the parameter of the method AnyNumberOfArguments is of type int[], you can pass an int array, or because of the params keyword, you can pass one or any number of int values:
If arguments of different types should be passed to methods, you can use an object array:
Now it is possible to use any type calling this method:
If the params keyword is used with multiple parameters that are defined with the method signature, params can be used only once, and it must be the last parameter:
Now that you’ve looked at the many aspects of methods, let’s get into constructors, which are a special kind of methods.
Constructors
The syntax for declaring basic constructors is a method that has the same name as the containing class and that does not have any return type:
It’s not necessary to provide a constructor for your class. We haven’t supplied one for any of the examples so far in this book. In general, if you don’t supply any constructor, the compiler generates a default one behind the scenes. It will be a very basic constructor that initializes all the member fields by zeroing them out (null reference for reference types, zero for numeric data types, and false for bools). Often, that is adequate; if not, you need to write your own constructor.
Constructors follow the same rules for overloading as other methods – that is, you can provide as many overloads to the constructor as you want, provided they are clearly different in signature:
However, if you supply any constructors that take parameters, the compiler does not automatically supply a default one. This is done only if you have not defined any constructors at all. In the following example, because a one-parameter constructor is defined, the compiler assumes that this is the only constructor you want to be available, so it does not implicitly supply any others:
If you now try instantiating a MyNumber object using a no-parameter constructor, you get a compilation error:
Note that it is possible to define constructors as private or protected, so that they are invisible to code in unrelated classes too:
This example hasn’t actually defined any public, or even any protected, constructors for MyNumber. This would actually make it impossible for MyNumber to be instantiated by outside code using the new operator (though you might write a public static property or method in MyNumber that can instantiate the class). This is useful in two situations:
• If your class serves only as a container for some static members or properties, and therefore should never be instantiated. With this scenario, you can declare the class with the modifier static. With this modifier the class can contain only static members and cannot be instantiated.
• If you want the class to only ever be instantiated by calling a static member function (this is the so-called factory pattern approach to object instantiation). An implementation of the Singleton pattern is shown in the following code snippet.
The Singleton class contains a private constructor, so you can instantiate it only within the class itself. To instantiate it, the static property Instance returns the field s_instance. If this field is not yet initialized (null), a new instance is created by calling the instance constructor. For the null check, the coalescing operator is used. If the left side of this operator is null, the right side of this operator is processed and the instance constructor invoked.
NOTE The coalescing operator is explained in detail in Chapter 8.
Calling Constructors from Other Constructors
You might sometimes find yourself in the situation where you have several constructors in a class, perhaps to accommodate some optional parameters for which the constructors have some code in common. For example, consider the following:
Both constructors initialize the same fields. It would clearly be neater to place all the code in one location. C# has a special syntax known as a constructor initializer to enable this:
In this context, the this keyword simply causes the constructor with the nearest matching parameters to be called. Note that any constructor initializer is executed before the body of the constructor. Suppose that the following code is run:
In this example, the two-parameter constructor executes before any code in the body of the one-parameter constructor (though in this particular case, because there is no code in the body of the one-parameter constructor, it makes no difference).
A C# constructor initializer may contain either one call to another constructor in the same class (using the syntax just presented) or one call to a constructor in the immediate base class (using the same syntax, but using the keyword base instead of this). It is not possible to put more than one call in the initializer.
Static Constructors
One feature of C# is that it is also possible to write a static no-parameter constructor for a class. Such a constructor is executed only once, unlike the constructors written so far, which are instance constructors that are executed whenever an object of that class is created:
One reason for writing a static constructor is if your class has some static fields or properties that need to be initialized from an external source before the class is first used.
The .NET runtime makes no guarantees about when a static constructor will be executed, so you should not place any code in it that relies on it being executed at a particular time (for example, when an assembly is loaded). Nor is it possible to predict in what order static constructors of different classes will execute. However, what is guaranteed is that the static constructor will run at most once, and that it will be invoked before your code makes any reference to the class. In C#, the static constructor is usually executed immediately before the first call to any member of the class.
Note that the static constructor does not have any access modifiers. It’s never called explicitly by any other C# code, but always by the .NET runtime when the class is loaded, so any access modifier such as public or private would be meaningless. For this same reason, the static constructor can never take any parameters, and there can be only one static constructor for a class. It should also be obvious that a static constructor can access only static members, not instance members, of the class.
It is possible to have a static constructor and a zero-parameter instance constructor defined in the same class. Although the parameter lists are identical, there is no conflict because the static constructor is executed when the class is loaded, but the instance constructor is executed whenever an instance is created. Therefore, there is no confusion about which constructor is executed or when.
If you have more than one class that has a static constructor, the static constructor that is executed first is undefined. Therefore, you should not put any code in a static constructor that depends on other static constructors having been or not having been executed. However, if any static fields have been given default values, these are allocated before the static constructor is called.
The next example illustrates the use of a static constructor. It is based on the idea of a program that has user preferences (which are presumably stored in some configuration file). To keep things simple, assume just one user preference – a quantity called BackColor that might represent the background color to be used in an application. Because we don’t want to get into the details of writing code to read data from an external source here, assume also that the preference is to have a background color of red on weekdays and green on weekends. All the program does is display the preference in a console window, but that is enough to see a static constructor at work.
The class UserPreferences is declared with the static modifier; thus it cannot be instantiated and can only contain static members. The static constructor initializes the BackColor property depending on the day of the week (code file StaticConstructorSample/UserPreferences.cs):
This code makes use of the System.DateTime struct that is supplied with the .NET Framework. DateTime implements a static property Now that returns the current time. DayOfWeek is an instance property of DateTime that returns an enum value of type DayOfWeek.
Color is defined as an enum type and contains a few colors. The enum types are explained in detail later in the section Enums (code file StaticConstructorSample/Enum.cs):
The Main method just invokes the WriteLine method and writes the user preferences back color to the console (code file StaticConstructorSample/Program.cs):
Compiling and running the preceding code results in the following output:
Of course, if the code is executed during the weekend, your color preference would be Green.
Readonly Members
If you do not want to change a data member after initialization, the readonly keyword can be used. Let’s get into the details of readonly fields and readonly properties.
Readonly Fields
To guarantee that fields of an object cannot be changed, fields can be declared with the readonly modifier. Fields with the readonly modifier can be assigned only values from constructors. This is different from the const modifier. With the const modifier, the compiler replaces the variable by its value everywhere it is used. The compiler already knows the value of the constant. Read-only fields are assigned during runtime from a constructor. Contrary to const fields, read-only fields can be instance members. For using a read-only field as a class member, the static modifier needs to be assigned to the field.
Suppose that you have a program that edits documents, and for licensing reasons you want to restrict the number of documents that can be opened simultaneously. Assume also that you are selling different versions of the software, and it’s possible for customers to upgrade their licenses to open more documents simultaneously. Clearly, this means you can’t hard-code the maximum number in the source code. You would probably need a field to represent this maximum number. This field has to be read in – perhaps from a registry key or some other file storage – each time the program is launched. Therefore, your code might look something like this:
In this case, the field is static because the maximum number of documents needs to be stored only once per running instance of the program. This is why it is initialized in the static constructor. If you had an instance readonly field, you would initialize it in the instance constructor(s). For example, presumably each document you edit has a creation date, which you wouldn’t want to allow the user to change (because that would be rewriting the past!).
As noted earlier, date is represented by the class System.DateTime. The following code initializes the _creationTime field in the constructor using the DateTime struct. After initialization of the Document class, the creation time cannot be changed anymore:
CreationDate and MaxDocuments in the previous code snippet are treated like any other field, except that because they are read-only they cannot be assigned outside the constructors:
It’s also worth noting that you don’t have to assign a value to a readonly field in a constructor. If you don’t do so, it is left with the default value for its particular data type or whatever value you initialized it to at its declaration. That applies to both static and instance readonly fields.
Readonly Properties
It is possible to create a read-only property by simply omitting the set accessor from the property definition. Thus, to make Name a read-only property, you would do the following:
Конец ознакомительного фрагмента. Купить книгу