Chapter 4

Holding a Class Responsible

IN THIS CHAPTER

Bullet Protecting a class

Bullet Working with class constructors

Bullet Constructing static or class members

Bullet Working with expression-bodied members

A class must be held responsible for its actions. Just as a microwave oven shouldn’t burst into flames if you press the wrong key, so a class shouldn’t allow itself to roll over and die when presented with incorrect data.

To be held responsible for its actions, a class must ensure that its initial state is correct and then control its subsequent state so that it remains valid. C# provides both these capabilities. This chapter discusses how to make your classes responsible members of the code community. After all, you wouldn’t want to design a renegade class that runs amok and creates chaos.

Remember You don’t have to type the source code for this chapter manually. In fact, using the downloadable source is a lot easier. You can find the source for this chapter in the CSAIO4D2EBK02CH04 folder of the downloadable source. See the Introduction for details on how to find these source files.

Restricting Access to Class Members

As you saw in previous chapters of this minibook, best practice for defining classes is to ensure that each member only provides the level of visibility absolutely required by other class members. Making everything private is the best idea when you can achieve this level of hiding (which definitely isn't always possible). Consider a BankAccount program that maintains a balance data member to retain the balance in each account. Making that data member public puts everyone on the honor system.

Most banks aren't nearly so forthcoming as to leave a pile of money and a register for you to mark down every time you add money to or take money away from the pile. After all, you may forget to mark your withdrawals in the register. Controlling access avoids little mistakes, such as forgetting to mark a withdrawal here or there, and manages to avoid some truly big mistakes with withdrawals. The following sections provide you with techniques for maintaining control over how other developers interact with the classes you create.

A public example of public BankAccount

The BankAccount example declares all its methods public but declares its data members, including _accountNumber and _balance, as private. The example leaves the variables in an incorrect state to make a point. The following code contains the BankAccount class uses for the example:

// BankAccount -- Define a class that represents a simple account.
public class BankAccount
{
private static int _nextAccountNumber = 1000;
private int _accountNumber;

// Maintain the balance as a double variable.
private double _balance;

// Init -- Initialize a bank account with the next
// account id and a balance of 0.
public void InitBankAccount()
{
_accountNumber = ++_nextAccountNumber;
_balance = 0.0;
}

// Balance property only obtains a balance.
public double Balance
{ get => _balance; }

// AccountNumber property
public int AccountNumber
{ get => _accountNumber; set => _accountNumber = value; }

// Deposit -- Any positive deposit is allowed.
public void Deposit(double amount)
{
if (amount > 0.0)
{
_balance += amount;
}
}

// Withdraw -- You can withdraw any amount up to the
// balance; return the amount withdrawn.
public double Withdraw(double withdrawal)
{
if (_balance <= withdrawal)
{
withdrawal = _balance;
}
_balance -= withdrawal;
return withdrawal;
}

// Return the account data as a string.
public override string ToString()
{
return $"{AccountNumber} = {Balance:C}";
}
}

The BankAccount class provides an InitBankAccount() method to initialize the members of the class, a Deposit() method to handle deposits, and a Withdraw() method to perform withdrawals. The Deposit() and Withdraw() methods even provide some rudimentary rules, such as “You can't deposit a negative number” and “You can’t withdraw more than you have in your account” (both good rules for a bank, as I’m sure you’ll agree). However, everyone would be on the honor system if _balance were accessible to external methods, which is why you make it private. (In this context, external means external to the class but within the same program.) The honor system can be a problem on big programs written by teams of programmers. It can even be a problem for you (and me), given general human fallibility. Here’s the Main() method, which exercises this code:

static void Main(string[] args)
{
Console.WriteLine("This program doesn't compile.");

// Open a bank account.
Console.WriteLine("Create a bank account object");
BankAccount ba = new BankAccount();
ba.InitBankAccount();

// Accessing the balance via the Deposit() method is okay --
// Deposit() has access to all the data members.
ba.Deposit(10);

// Accessing the data member directly is a compile-time error.
Console.WriteLine("Just in case you get this far the following is "
+ "supposed to generate a compile error");
ba._balance += 10;
Console.Read();
}

All that Main() does is create a new bank account, initialize it, then add money to it. Notice the attempt to access _balance directly using ba._balance += 10;.

Remember Well-written code with rules that the compiler can enforce saves everyone from the occasional bullet to the big toe. Before you get too excited, however, notice that the program doesn't build. Attempts to do so generate this error message:

'BankAccount.BankAccount._balance' is inaccessible due to its protection level.

The error message seems a bit hard to understand because that’s how error messages are, for the most part (writing a truly understandable error message is incredibly tough). The crux of the problem is that _balance is private, which means no one can see it. The statement ba._balance += 10; is illegal because _balance isn't accessible to Main(), a method outside the BankAccount class. Replacing this line with ba.Deposit(10); solves the problem. The BankAccount.Deposit() method is public and therefore accessible to Main() and other parts of your program.

Remember The default access type is private. Not declaring a class member's access type explicitly is the same as declaring it private. However, you should include the private keyword to remove any doubt. Good programmers make their intentions explicit, which is another way to reduce errors.

Jumping ahead — other levels of security

Warning Understanding this section depends on your having some knowledge of inheritance (see Chapter 6 in this minibook) and namespaces (see Chapter 9 in this minibook). You can skip this section for now if you want, but just know that it's here when you need it. C# provides these levels of security:

  • A public member is accessible to any class in the program.
  • A private member is accessible only from the current class.
  • A protected member is accessible from the current class and any of its subclasses.
  • An internal member is accessible from any class within the same program module or assembly.

    Technicalstuff A C# module, or assembly, is a separately compiled piece of code, either an executable program in an .EXE file or a supporting library module in a .DLL file. A single namespace can extend across multiple assemblies. (Chapter 9 in this minibook explains C# assemblies and namespaces and discusses access levels other than public and private.)

  • An internal protected member is accessible from the current class and any subclass, and from classes within the same module.
  • A private protected member is accessible by code in the same assembly by code in the same class or by a type that is derived from that class.

Keeping a member hidden by declaring it private offers the maximum amount of security. However, in many cases, you don't need that level of security. After all, the members of a subclass already depend on the members of the base class, so protected offers a comfortable level of security.

Why You Should Worry about Access Control

Declaring the internal members of a class public is a bad idea for at least these reasons:

  • With all data members public, you can't easily determine when and how data members are being modified. Why bother building safety checks into the Deposit() and Withdraw() methods? In fact, why even bother with these methods? Any method of any class can modify these elements at any time. If other methods can access these data members, they almost certainly will.

    Your BankAccount program may execute for an hour or so before you notice that one of the accounts has a negative balance. The Withdraw() method would have ensured that this situation didn't happen, so obviously another method accessed the balance without going through Withdraw(). Figuring out which method is responsible and under which conditions is a difficult problem.

  • Exposing all data members of the class makes the interface too complicated. As a programmer using the BankAccount class, you don't want to know about the internal workings of the class. You just need to know that you can deposit and withdraw funds. It’s like a candy machine that has 50 buttons versus one with just a few buttons — the ones you need.
  • Exposing internal elements leads to a distribution of the class rules. For example, my BankAccount class doesn’t allow the balance to be negative under any circumstances. That required business rule should be isolated within the Withdraw() method. Otherwise, you have to add this check everywhere the balance is updated.

    Sometimes a bank decides to change the rules so that valued customers are allowed to carry slightly negative balances for a short period, to avoid unintended overdrafts. Then you have to search through the program to update every section of code that accesses the balance, to ensure that the safety checks are changed.

Tip Make your classes and methods no more accessible than necessary. This advice isn't meant to cause paranoia about snoopy hackers so much as it is to suggest a prudent step that helps reduce errors as you code. Use private, if possible, and then escalate to protected, private protected, internal, internal protected, or public as necessary.

Accessor methods

If you look more carefully at the BankAccount class, you see a few other methods. One, ToString(), returns a string version of the account fit for presentation to any Console.WriteLine() for display. However, displaying the contents of a BankAccount object may be difficult if its contents are inaccessible. The class should have the right to decide how it is displayed.

In addition, you see two getter methods and one setter method in the form of the Balance and AccountNumber properties. You may wonder why it's important to declare a data member such as _balance as private, but to provide a public Balance property to return its value:

  • Balance doesn't provide a way to modify _balance — it merely returns its value. The balance is read-only. To use the analogy of an actual bank, you can look at your balance any time you want; you just can’t withdraw money from your account without using the bank’s withdrawal mechanism.
  • Balance hides the internal format of the class from external methods. Balance may perform an extensive calculation by reading receipts, adding account charges, and accounting for any other amounts your bank may want to subtract from your balance. External methods don't know and don’t care. Of course, you care which fees are being charged — you just can’t do anything about them, short of changing banks.

Finally, Balance provides a mechanism for making internal changes to the class without the need to change the interface seen by users of BankAccount. If the Federal Deposit Insurance Corporation (FDIC) mandates that your bank store deposits differently, the mandate shouldn't change the way you access your account.

Working with init-only setters

C# 9.0 introduces a new technique for working with properties where you can set the property only once, but then the property becomes immutable (unchangeable). Having immutable properties is good when you don’t want to allow changes beyond that initial setup, such as the identification numbers of club members. The member receives an identifier once, but then retains that identifier permanently. The CreditMember class in the InitOnly example shows how a class for quickly identifying the credit limit of a club member might work.

internal class CreditMember
{
internal int Id { get; init; }
internal string Name { get; set; }
internal decimal Limit { get; set; }

public override string ToString()
{
return $"{Name}, member ID {Id}, has a " +
$"limit of {Limit:C}";
}

internal protected CreditMember(int MemberId)
{
Id = MemberId;
}
}

The code shows three properties, an override for ToString(), and something new, a constructor. A constructor builds an object based on the blueprint provided by the class description. You find out more about them in the “Getting Your Objects Off to a Good Start — Constructors” section of this chapter. The constructor has an access level of internal protected so that any class that inherits this class also inherits the constructor.

Remember Notice also that the Id property has a substitution for set; in the form of init;. This entry means that you can set the property once during initialization, but not afterward. This is the reason that the constructor sets the Id property value. To make the functionality work from a standard Windows console application, you must add the following code right after the using statements in the application, but before the namespace InitOnly declaration:

namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit { }
}

The code that appears here doesn't actually do anything — it simply gets rid of the compiler error message. An alternative solution to including this code is to create a .NET Core application, instead of a .NET Framework console application, as shown in the InitOnly2 program. Note that you must select .NET 6.0 (Current) in the Target Framework field of the Console Application Wizard. Book 5 Chapter 5 tells you more about .NET Core applications.

The Main() method instantiates the CreditMember object, Sam, and then sets values in it. Afterward, it prints out the Sam object information. Here's the short code to test the InitOnly program.

static void Main(string[] args)
{
CreditMember Sam = new CreditMember(1);
Sam.Name = "Sam Jones";
Sam.Limit = 5000;

Console.WriteLine(Sam.ToString());
Console.ReadLine();
}

If you were to attempt to set Id at this point, you’d see an error message telling you that you can’t. Since this is a C# 9.0 example, you must add the following entry to the InitOnly.csproj file:

<PropertyGroup>
<LangVersion>9.0</LangVersion>
</PropertyGroup>

When you run this program, you see this output:

Sam Jones, member ID 1, has a limit of $5,000.00

Access control to the rescue — an example

The following DoubleBankAccount program demonstrates a potential flaw in the BankAccount program. The following listing shows Main() — the only portion of the program that differs from the earlier BankAccount program:

static void Main(string[] args)
{
// Open a bank account.
Console.WriteLine("Create a bank account object");
BankAccount ba = new BankAccount();
ba.InitBankAccount();

// Make a deposit.
double deposit = 123.454;
Console.WriteLine($"Depositing {deposit:C}");
ba.Deposit(deposit);

// Account balance
Console.WriteLine(ba.ToString());

// Here's the problem.
double fractionalAddition = 0.002;
Console.WriteLine($"Adding {fractionalAddition:C}");
ba.Deposit(fractionalAddition);

// Resulting balance
Console.WriteLine(ba.ToString());
Console.Read();
}

The Main() method creates a bank account and then deposits $123.454, an amount that contains a fractional number of cents. Main() then deposits a small fraction of a cent to the balance and displays the resulting balance. The output from this program appears this way:

Create a bank account object
Depositing $123.45
Account = #1001 = $123.45
Adding $0.00
Resulting account = #1001 = $123.46

Users start to complain: “I just can't reconcile my checkbook with my bank statement.” Apparently, the program has a bug.

The problem, of course, is that $123.454 shows up as $123.45. To avoid the problem, the bank decides to round deposits and withdrawals to the nearest cent. Deposit $123.454 and the bank takes that extra 0.4 cent. On the other side, the bank gives up enough 0.4 amounts that everything balances out in the long run. Well, in theory, it does.

The easiest way to solve the rounding problem is by converting the bank accounts to decimal and using the Decimal.Round() method, as shown in BankAccount class of the DecimalBankAccount program:

// BankAccount -- Define a class that represents a simple account.
internal class BankAccount
{
private static int _nextAccountNumber = 1000;
private int _accountNumber;

// Maintain the balance as a double variable.
private decimal _balance;

// Init -- Initialize a bank account with the next
// account id and a balance of 0.
internal protected void InitBankAccount()
{
_accountNumber = ++_nextAccountNumber;
_balance = 0.0M;
}

// Balance property only obtains a balance.
internal decimal Balance
{ get => _balance; }

// AccountNumber property
internal int AccountNumber
{ get => _accountNumber; set => _accountNumber = value; }

// Deposit -- Any positive deposit is allowed.
internal void Deposit(decimal amount)
{
if (amount > 0.0M)
{
// Round off the double to the nearest cent before depositing.
decimal temp = amount;
temp = Decimal.Round(temp, 2);

_balance += temp;
}
}

// Withdraw -- You can withdraw any amount up to the
// balance; return the amount withdrawn.
internal decimal Withdraw(decimal withdrawal)
{
decimal temp = withdrawal;
temp = Decimal.Round(temp, 2);

if (_balance <= temp)
{
temp = _balance;
}
_balance -= temp;
return temp;
}

// Return the account data as a string.
public override string ToString()
{
return $"{AccountNumber} = {Balance:C}";
}
}

This version of the example changes all internal representations to decimal values, a type better adapted to handling bank account balances than double in any case. The Deposit() and Withdrawal() methods now use the Decimal.Round() method to round the deposit amount to the nearest cent before making the deposit. Notice that the access levels are now appropriately set for this class. Note that you must also change Main() to use the correct data types, but the compiler will warn you about that issue. The output from the program is now as expected:

Create a bank account object
Depositing $123.45
Account = #1001 = $123.45
Adding $0.00
Resulting account = #1001 = $123.45

Defining Class Properties

C# defines a construct known as a property, a method-like construction that allows safe access to data fields within a class. The field contains the actual variable; the property provides access to that variable. You have already seen properties used in the BankAccount class examples in this chapter and in other examples in previous chapters. C# supports a number of property constructions that include:

  • Backing fields: This is an older style of property that doesn't use any shortcuts. It actually provides a getter and/or setter method that interacts with the underlying field directly such as:

    private int _myProp = 0;
    internal int MyProp
    {
    get { return _myProp; }
    set { _myProp = value; }
    }

    This form of property is useful when you need to perform data manipulations as part of working with the underlying field. For example, the property might handle time values in hours, but represent them internally as milliseconds, so you need to modify the values during the setting and getting process.

  • Expression body: You can use this form to assign an expression to the getter and setter. This shortcut method works well for many purposes. It uses the => operator to define the division between the getter or setter and the expression. You usually see it used something like this:

    private int _myProp = 0;
    internal int MyProp
    { get => _myProp; set => _myProp = value; }

    When using this form, the expression can be more complex than just a variable and the getter need not be specifically included if the property has only a setter, such as this form:

    private string _firstName = "John";
    private string _lastName = "Smith";
    internal string Name => $"{_firstName} {_lastName}";

  • Auto-implemented: This is the shortest way to create a property, but also allows the least flexibility. You don’t even have to provide a private variable to use it. This version looks like this:

    internal int MyProp
    { get; set; }

Tip By convention, the name of a property begins with a capital letter. Note that properties don’t have parentheses: It’s Balance, not Balance().

Technicalstuff Properties aren't necessarily inefficient. The C# compiler can optimize a simple accessor to the point that it generates no more machine code than accessing the data member directly does. This concept is important, not only to an application program but also to C# itself. The C# library uses properties throughout, and you should, too. Use properties to access class data members, even from methods in the same class.

Static properties

A static (class) data member may be exposed through a static property, as shown in this simplistic example (note its compact layout):

public class BankAccount
{
private static int _nextAccountNumber = 1000;
public static int NextAccountNumber { get {return _nextAccountNumber += 1; }}
// …
}

The NextAccountNumber property is accessed through the class as follows because it isn’t an instance property (it’s declared static):

// Read the account number property.
int value = BankAccount.NextAccountNumber;

(In this example, value is outside the context of a property, so it isn't a reserved word.)

Properties with side effects

A get operation can perform extra work other than simply retrieving the associated property, as shown here:

public static int AccountNumber
{
// Retrieve the property and set it up for the
// next retrieval by incrementing it.
get { return ++_nextAccountNumber; }
}

This property increments the static account number member before returning the result. This action probably isn’t a good idea, however, because the user of the property receives no clue that anything is happening other than the actual reading of the property. Incrementing _nextAccountNumber is a side effect.

Remember Like the accessor methods that they mimic, properties shouldn't change the state of the class other than, say, setting a data member’s value. Both properties and methods generally should avoid side effects because they can lead to subtle bugs. Change a class as directly and explicitly as possible.

Accessors with access levels

It’s usually a good idea to declare properties (when possible) as something other than public. You can declare them at any appropriate level, even private, if the accessor is used only inside its class. (The upcoming example marks the Name property internal, which is the best option for classes that aren't part of a library or API and only used within the host application.)

You can even adjust the access levels of the get and set portions of an accessor individually. Suppose that you don't want to expose the set accessor outside your class — it’s for internal use only. You can write the property like this:

internal string Name { get; private set; }

Using Target Typing for Your Convenience

Target typing refers to the ability of the compiler to derive the appropriate type for a variable based on context, rather than actual code. You see it used relatively often, but C# 9.0 provides two new ways to use target typing. The first way is when you declare variables, such as in a list. It’s no longer necessary to work your fingers to the bone; let the compiler do the heavy lifting. The following class and enumeration appear in the TargetType1 program and provide the means for testing what target typing means in this case (note that you must configure the application to use C# 9.0 by modifying the .csproj file):

internal enum FoodGroups
{
Meat,
Vegetables,
Fruit,
Grain,
Dairy
}

internal class MyFavorteFoods
{
internal int Rank { get; set; }
internal string Name { get; set; }
internal FoodGroups Group { get; set; }

public MyFavorteFoods(int Position,
string Food, FoodGroups Category)
{
Rank = Position;
Name = Food;
Group = Category;
}
}

The example class is simple—it provides three properties, one of which relies on the FoodGroups enumeration, and a constructor to instantiate objects. Notice that the constructor is public to ensure proper access. Here's the Main() code used to work with the class:

static void Main(string[] args)
{
var Foods = new List<MyFavorteFoods>
{
new (1, "Apples", FoodGroups.Fruit),
new (2, "Steaks", FoodGroups.Meat),
new (3, "Asparagus", FoodGroups.Vegetables)
};

foreach (MyFavorteFoods Item in Foods)
Console.WriteLine($"Food #{Item.Rank} is {Item.Name} " +
$"of {Item.Group} food category.");
Console.ReadLine();
}

The most important thing you should notice is that the Foods list construction doesn't require any type information. The compiler automatically provides the correct type. When you run this application, you see the following output:

Food #1 is Apples of Fruit food category.
Food #2 is Steaks of Meat food category.
Food #3 is Asparagus of Vegetables food category.

The second case is with conditional compilation situations. Again, the easiest way to understand how this works is to see an example. The TargetType2 example begins with the MyFavorteFoods class found in the TargetType1 example. To see how things work, you create two derived classes as shown here (again, ensuring your project supports C# 9.0):

internal class MyLuxuryFoods : MyFavorteFoods
{
internal decimal HowMuch { get; set; }

public MyLuxuryFoods(int Position,
string Food, FoodGroups Category, decimal Cost) :
base(Position, Food, Category)
{
HowMuch = Cost;
}
}

internal class MyComfortFoods: MyFavorteFoods
{
internal string HowOften { get; set; }
public MyComfortFoods(int Position,
string Food, FoodGroups Category, string Time) :
base(Position, Food, Category)
{
HowOften = Time;
}
}

Both subclasses rely on MyFavorteFoods as a starting point. However, they both add something different. Generally, you couldn't use these two classes with the ?? null-coalescing operator. However, the following code does work with C# 9.0:

static void Main(string[] args)
{
MyLuxuryFoods GreatFood = new MyLuxuryFoods(
1, "Salmon", FoodGroups.Meat, 35.95M);
MyComfortFoods SatisfyingFood = new MyComfortFoods(
1, "Oatmeal", FoodGroups.Grain, "Weekly");
MyFavorteFoods Choice = GreatFood ?? SatisfyingFood;
Console.WriteLine(Choice.Name);
Console.ReadLine();
}

So, you might wonder what MyFavorteFoods Choice = GreatFood ?? SatisfyingFood; actually means. If GreatFood is available, then eat it, otherwise, eat SatisfyingFood. So, if you were to add GreatFood = null; before instantiating Choice, the output would be Oatmeal instead of Salmon.

Dealing with Covariant Return Types

A covariant return type is one in which you can return a type that is more detailed (at a lower level in the class hierarchy) than the type that would normally be returned. This particular feature only works if you use the .NET Core version of the Console Application template and select .NET 6.0 in the Target Framework field of the wizard. The best way to understand how this feature works is to look at an example. The CovariantReturn program provides a basic example that focuses on the C# record type (https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/records). The following code shows the class hierarchy:

public abstract record Number
{ public int Value { get; set; } }

public abstract record BadgeNumber
{ public virtual Number Id { get; } }

public record EmployeeID : Number
{ public string FullName { get; set; } }

public record ThisPerson : BadgeNumber
{
public ThisPerson(int Identifier, string Name)
{
Id = new EmployeeID
{
FullName = Name,
Value = Identifier
};
}

public override Number Id { get; }
}

The example begins with a base type, Number, which contains a single property, Value, of type int. The second base type, BadgeNumber, also contains a single property, but this one is virtual and it uses Number as its type. Both of these base types are abstract (https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/abstract), which means you can't instantiate them.

The next two types are derived from Number and BadgeNumber. EmployeeID adds a new property, FullName, of type string. This means that EmployeeID contains two properties: Value and FullName.

The magic of covariant return types makes its appearance in ThisPerson. Notice that ThisPerson has a constructor and initializes the Id property found in BadgeNumber to an EmployeeID, rather than to an int. So, Id now contains a more detailed type than originally expected. The ThisPerson record also contains a property that returns Id as type Number, rather than type int, as you might expect. So, how does this all work? Is it mumbo jumbo or real code? The Main() method demonstrates it does work in C# 9.0 when using the .NET 6.0 framework:

static void Main(string[] args)
{
var Josh = new ThisPerson(22, "Josh");
Console.WriteLine(Josh);
Console.WriteLine(Josh.GetType());

BadgeNumber ThisNumber = Josh;
Console.WriteLine(ThisNumber);
Console.WriteLine(ThisNumber.GetType());
}

The code begins by creating a new ThisPerson, Josh, who has badge number 22 and a name of, well, Josh. The code then creates another object, ThisNumber, of type BadgeNumber, and tries to assign Josh to it. You might think that this code really shouldn't work, but when you run it you see this output that demonstrates that the compiler assigns the proper object of the proper type to ThisNumber, even though it’s supposedly of type BadgeNumber.

ThisPerson { Id = EmployeeID { Value = 22, FullName = Josh } }
CovariantReturn.ThisPerson
ThisPerson { Id = EmployeeID { Value = 22, FullName = Josh } }
CovariantReturn.ThisPerson

Getting Your Objects Off to a Good Start — Constructors

Remember Controlling class access is only half the problem: An object needs a good start in life if it is to grow. A class can supply an initialization method that the application calls to get things started, but the application could forget to call the method. The class starts out with garbage, and the situation gets no better after that. If you want to hold the class accountable, you have to ensure that it has a chance to start out correctly. C# solves that problem by calling the initialization method for you — for example:

MyObject mo = new MyObject();

Remember In other words, this statement not only grabs an object from a special memory area but also initializes that object's members. Keep the terms class and object separate in your mind. Cat is a class. An instance of Cat named Striper is an object of class Cat.

The C#-Provided Constructor

C# keeps track of whether a variable has been initialized and doesn't allow you to use an uninitialized variable. For example, the following code chunk generates a compile-time error:

public static void Main(string[] args)
{
int n;
double d;
double calculatedValue = n + d;
}

C# tracks the fact that the local variables n and d haven't been assigned a value and doesn’t allow them to be used in the expression. Compiling this tiny program generates these compiler errors:

Use of unassigned local variable 'n'
Use of unassigned local variable 'd'

By comparison, C# provides a default constructor that initializes the data members of an object to

  • 0 for numbers
  • false for Booleans
  • null for object references

Consider the MyObject class from the UseConstructor program example:

internal class MyObject
{
internal int n;
internal MyObject nextObject;
}

You can work with it using the following code:

static void Main(string[] args)
{
// First create an object.
MyObject localObject = new MyObject();
Console.WriteLine("localObject.n is {0}", localObject.n);

if (localObject.nextObject == null)
{
Console.WriteLine("localObject.nextObject is null");
}
Console.Read();
}

This program defines a class MyObject, which contains both a simple data member n of type int and a reference to an object, nextObject (both declared internal). The Main() method creates a MyObject and then displays the initial contents of n and nextObject. The output from executing the program appears this way:

localObject.n is 0
localObject.nextObject is null

When the object is created, C# executes a small piece of code that the compiler provides to initialize the object and its members. Left to their own devices, the data members localObject.n and nextObject would contain random, garbage values.

Remember The code that initializes values when they're created is the default constructor. It constructs the class, in the sense of initializing its members. Thus C# ensures that an object starts life in a known state: all zeros, nulls, or false values, depending on type. This concept affects only data members of the class, not local variables in a method.

Replacing the Default Constructor

Although the compiler automatically initializes all instance variables to the appropriate values, for many classes (probably most classes), the default value isn’t a valid state. Consider the following BankAccount class from earlier in this chapter:

internal class BankAccount
{
private int _accountNumber;
private double _balance;
// …other members
}

Although an initial balance of 0 is probably okay, an account number of 0 definitely isn't the hallmark of a valid bank account.

At this point in the chapter, the BankAccount class includes the InitBankAccount() method to initialize the object. However, this approach puts too much responsibility on the application software using the class. If the application fails to invoke the InitBankAccount() method, the bank account methods may not work, through no fault of their own.

Remember A class shouldn't rely on methods such as InitBankAccount() to start the object in a valid state. To work around this problem, you can have your class provide its own explicit class constructor that C# calls automatically when the object is created. The constructor could have been named Init(), Start(), or Create(), but C# requires the constructor to carry the name of the class. Thus a constructor for the BankAccount class appears this way:

public void Main(string[] args)
{
BankAccount ba = new BankAccount(); // This invokes the constructor.
}

public class BankAccount
{
// Bank accounts start at 1000 and increase sequentially.
private static int _nextAccountNumber = 1000;

// Maintain the account number and balance for each object.
private int _accountNumber;
private double _balance;

// BankAccount constructor -- Here it is -- ta-da!
// Parentheses, possible arguments, no return type
public BankAccount()
{
_accountNumber = ++_nextAccountNumber;
_balance = 0.0;
}

// … other members …
}

The contents of the BankAccount constructor are the same as those of the original Init…() method. However, the way you declare and use the constructor differs:

  • The constructor always carries the same name as the class.
  • The constructor can take parameters (or not).
  • The constructor never has a return type, not even void.
  • Main() doesn't need to invoke any extra method to initialize the object when it’s created; no Init() is necessary.

Remember If you provide your own constructor, C# no longer supplies a default constructor. Your constructor replaces the default and becomes the only way to create an instance of your class.

Constructing something

Try out a constructor thingie. Consider the classes from the following program, DemonstrateCustomConstructor:

// MyObject -- Create a class with a noisy custom constructor
// and an internal data object.
public class MyObject
{
// This data member is a property of the class (it's static).
private static MyOtherObject _staticObj = new MyOtherObject();

// This data member is a property of each instance.
private MyOtherObject _dynamicObj;

// Constructor (a real chatterbox)
public MyObject()
{
Console.WriteLine("MyObject constructor starting");
Console.WriteLine("(Static data member constructed before " +
"this constructor)");
Console.WriteLine("Now create nonstatic data member dynamically:");
_dynamicObj = new MyOtherObject();
Console.WriteLine("MyObject constructor ending");
}
}

// MyOtherObject -- This class also has a noisy constructor but
// no internal members.
public class MyOtherObject
{
public MyOtherObject()
{
Console.WriteLine("MyOtherObject constructing");
}
}

The Main() function merely starts the construction process, as shown here:

static void Main(string[] args)
{
Console.WriteLine("Main() starting");
Console.WriteLine("Creating a local MyObject in Main():");
MyObject localObject = new MyObject();
Console.Read();
}

Executing this program generates the following output:

Main() starting
Creating a local MyObject in Main():
MyOtherObject constructing
MyObject constructor starting
(Static data member constructed before this constructor)
Now create nonstatic data member dynamically:
MyOtherObject constructing
MyObject constructor ending
Press Enter to terminate…

The following steps reconstruct what just happened:

  1. The program starts, and Main() outputs the initial message and announces that it's about to create a local MyObject.
  2. Main() creates a localObject of type MyObject.
  3. MyObject contains a static member _staticObj of class MyOtherObject.

    Remember All static data members are initialized before the first MyObject() constructor runs. In this case, C# populates _staticObj with a newly created MyOtherObject before passing control to the MyObject constructor. This step accounts for the third line of output.

  4. The constructor for MyObject is given control. It outputs the initial message, MyObject constructor starting, and then notes that the static member was already constructed before the MyObject() constructor began:

    (Static data member constructed before this constructor)

  5. After announcing its intention with Now create nonstatic data member dynamically, the MyObject constructor creates an object of class MyOtherObject using the new operator, generating the second MyOtherObject constructing message as the MyOtherObject constructor is called.
  6. Control returns to the MyObject constructor, which returns to Main().

Initializing an object directly with an initializer

Besides letting you initialize data members in a constructor, C# enables you to initialize data members directly by using initializers. Thus, you could write the BankAccount class as follows:

public class BankAccount
{
// Bank accounts start at 1000 and increase sequentially.
private static int _nextAccountNumber = 1000;

// Maintain the account number and balance for each object.
private int _accountNumber = ++_nextAccountNumber;
private double _balance = 0.0;

// … other members …
}

Here's the initializer business. Both _accountNumber and _balance are assigned a value as part of their declaration, which has the same effect as a constructor but without having to do the work in it.

Be clear about exactly what's happening. You may think that this statement sets _balance to 0.0 right now. However, _balance exists only as a part of an object. Thus, the assignment isn't executed until a BankAccount object is created. In fact, this assignment is executed every time an object is created.

Note that the static data member _nextAccountNumber is initialized the first time the BankAccount class is accessed; that's the first time you access any method or property of the object owning the static data member, including the constructor.

Remember After the static member is initialized, it isn’t reinitialized every time you construct a BankAccount instance. That’s different from the instance members. Initializers are executed in the order of their appearance in the class declaration. If C# encounters both initializers and a constructor, the initializers are executed before the body of the constructor.

Seeing that construction stuff with initializers

In the DemonstrateCustomConstructor program, move the call new MyOtherObject() from the MyObject constructor to the declaration itself, as follows (see the bold text), modify the second WriteLine() statement as shown, and then rerun the program:

public class MyObject
{
// This member is a property of the class (it's static).
private static MyOtherObject _staticObj = new MyOtherObject();

// This member is a property of each instance.
private MyOtherObject _dynamicObj = new MyOtherObject(); // <- Here.

public MyObject()
{
Console.WriteLine("MyObject constructor starting");
Console.WriteLine(
"Both data members initialized before this constructor)");
// _dynamicObj construction was here, now moved up.
Console.WriteLine("MyObject constructor ending");
}
}

Compare the following output from this modified program with the output from its predecessor, DemonstrateCustomConstructor:

Main() starting
Creating a local MyObject in Main():
MyOtherObject constructing
MyOtherObject constructing
MyObject constructor starting
(Both data members initialized before this constructor)
MyObject constructor ending
Press Enter to terminate…

Initializing an object without a constructor

Suppose that you have a little class to represent a Student:

public class Student
{
public string Name { get; set; }
public string Address { get; set; }
public double GradePointAverage { get; set; }
}

A Student object has three public properties, Name, Address, and GradePointAverage, which specify the student's basic information. Normally, when you create a new Student object, you have to initialize its Name, Address, and GradePointAverage properties like this:

Student randal = new Student();
randal.Name = "Randal Sphar";
randal.Address = "123 Elm Street, Truth or Consequences, NM 00000";
randal.GradePointAverage = 3.51;

If Student had a constructor, you could do something like this:

Student randal = new Student
("Randal Sphar", "123 Elm Street, Truth or Consequences, NM, 00000", 3.51);

Sadly, however, Student lacks a constructor, other than the default one that C# supplies automatically — which takes no parameters. You can simplify that initialization with something that looks suspiciously like a constructor — well, sort of:

Student randal = new Student
{ Name = "Randal Sphar",
Address = "123 Elm Street, Truth or Consequences, NM 00000",
GradePointAverage = 3.51
};

The last two examples are different in this respect: The first one, using a constructor, shows parentheses containing two strings and one double value separated by commas, and the second one, using the new object-initializer syntax, has instead curly braces containing three assignments separated by commas. The syntax works something like this:

new LatitudeLongitude
{ assignment to Latitude, assignment to Longitude };

The object-initializer syntax lets you assign to any accessible set properties of the LatitudeLongitude object in a code block (the curly braces). The block is designed to initialize the object. Note that you can set only accessible properties this way, not private ones, and you can't call any of the object’s methods or do any other work in the initializer.

The object-initializer syntax is much more concise: one statement versus three. Also, it simplifies the creation of initialized objects that don’t let you do so through a constructor. The new object-initializer syntax doesn’t gain you much of anything besides convenience, but convenience when you’re coding is high on any programmer’s list. So is brevity. Besides, the feature becomes essential when you read about anonymous classes.

Tip Use the new object-initializer syntax to your heart’s content. The book uses it frequently, so you have plenty of examples. The help topic at https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/how-to-initialize-objects-by-using-an-object-initializer provides additional details.

Using Expression-Bodied Members

Expression-bodied members first appeared in C# 6.0 as a means to make methods and properties easier to define. In C# 7.0, expression-bodied members also work with constructors, destructors, property accessors, and event accessors.

Creating expression-bodied methods

The following example shows how you might have created a method before C# 6.0:

public int RectArea(Rectangle rect)
{
return rect.Height * rect.Width;
}

Remember When working with an expression-bodied member, you can reduce the number of lines of code to just one line, like this:

public int RectArea(Rectangle rect) => rect.Height * rect.Width;

Even though both versions perform precisely the same task, the second version is much shorter and easier to write. The trade-off is that the second version is also terse and can be harder to understand.

Defining expression-bodied properties

Expression-bodied properties work similarly to methods: You declare the property using a single line of code, like this:

public int RectArea => _rect.Height * _rect.Width;

The example assumes that you have a private member named _rect defined and that you want to get the value that matches the rectangle's area.

Defining expression-bodied constructors and destructors

In C# 7.0, you can use this same technique when working with a constructor. In earlier versions of C#, you might create a constructor like this one:

public EmpData()
{
_name = "Harvey";
}

In this case, the EmpData class constructor sets a private variable, _name, equal to "Harvey". The C# 7.0 version uses just one line but accomplishes the same task:

public EmpData() => _name = "Harvey";

Destructors work much the same as constructors. Instead of using multiple lines, you use just one line to define them.

Defining expression-bodied property accessors

Property accessors can also benefit from the use of expression-bodied members. Here is a typical C# 6.0 property accessor with both get and set methods:

private int _myVar;
public MyVar
{
get
{
return _myVar;
}
set
{
SetProperty(ref _myVar, value);
}
}

When working in C# 7.0, you can shorten the code using an expression-bodied member, like this:

private int _myVar;
public MyVar
{
get => _myVar;
set => SetProperty(ref _myVar, value);
}

Defining expression-bodied event accessors

As with property accessors, you can create an event accessor form using the expression-bodied member. Here’s what you might have used for C# 6.0:

private EventHandler _myEvent;
public event EventHandler MyEvent
{
add
{
_myEvent += value;
}
remove
{
_myEvent -= value;
}
}

The expression-bodied member form of the same event accessor in C# 7.0 looks like this:

private EventHandler _myEvent;
public event EventHandler MyEvent
{
add => _myEvent += value;
remove => _myEvent -= value;
}

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

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