Chapter 4
IN THIS CHAPTER
Protecting a class
Working with class constructors
Constructing static or class members
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.
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.
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;
.
'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.
public
member is accessible to any class in the program.private
member is accessible only from the current class.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.
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
.)
internal protected
member is accessible from the current class and any subclass, and from classes within the same module.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.
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.
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.
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.
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.
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
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
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:
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.
=>
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}";
internal int MyProp
{ get; set; }
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.)
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.
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; }
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
.
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
MyObject mo = new MyObject();
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
false
for Booleansnull
for object referencesConsider 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.
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.
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:
void
.Main()
doesn't need to invoke any extra method to initialize the object when it’s created; no Init()
is necessary.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:
Main()
outputs the initial message and announces that it's about to create a local MyObject
.Main()
creates a localObject
of type MyObject
.MyObject
contains a static member _staticObj
of class MyOtherObject
.
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.
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)
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.MyObject
constructor, which returns to Main()
.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
class as follows: BankAccount
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.
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…
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 string
s 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.
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.
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;
}
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.
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.
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.
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);
}
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;
}