All the code you have seen so far has taken the form of a single block, perhaps with some looping to repeat lines of code, and branching to execute statements conditionally. Performing an operation on your data has meant placing the code required right where you want it to work.
This kind of code structure is limited. Often, some tasks—such as finding the highest value in an array, for example—might need to be performed at several points in a program. You can place identical (or nearly identical) sections of code in your application whenever necessary, but this has its own problems. Changing even one minor detail concerning a common task (to correct a code error, for example) can require changes to multiple sections of code, which can be spread throughout the application. Missing one of these can have dramatic consequences and cause the whole application to fail. In addition, the application can get very lengthy.
The solution to this problem is to use functions . Functions in C# are a means of providing blocks of code that can be executed at any point in an application.
For example, you could have a function that calculates the maximum value in an array. You can use the function from any point in your code, and use the same lines of code in each case. Because you need to supply this code only once, any changes you make to it will affect this calculation wherever it is used. The function can be thought of as containing reusable code.
Functions also have the advantage of making your code more readable, as you can use them to group related code together. This way, your application body can be very short, as the inner workings of the code are separated out. This is similar to the way in which you can collapse regions of code together in the IDE using the outline view, and it gives your application a more logical structure.
Functions can also be used to create multipurpose code, enabling them to perform the same operations on varying data. You can supply a function with information to work with in the form of arguments, and you can obtain results from functions in the form of return values. In the preceding example, you could supply an array to search as an argument and obtain the maximum value in the array as a return value. This means that you can use the same function to work with a different array each time. A function definition consists of a name, a return type, and a list of parameters that specify the number and type of arguments that the function requires. The name and parameters of a function (but not its return type) collectively define the signature of a function.
This section describes how you can add functions to your applications and then use (call) them from your code. Starting with the basics, you look at simple functions that don't exchange any data with code that calls them, and then look at more advanced function usage. The following Try It Out gets things moving.
The simplest way to exchange data with a function is to use a return value. Functions that have return values evaluate to that value exactly the same way that variables evaluate to the values they contain when you use them in expressions. Just like variables, return values have a type.
For example, you might have a function called GetString()
whose return value is a string. You could use this in code, such as the following:
string myString;
myString = GetString();
Alternatively, you might have a function called GetVal()
that returns a double
value, which you could use in a mathematical expression:
double myVal;
double multiplier = 5.3;
myVal = GetVal() * multiplier;
When a function returns a value, you have to modify your function in two ways:
void
keyword.return
keyword to end the function execution and transfer the return value to the calling code.In code terms, this looks like the following in a console application function of the type you've been looking at:
static <
returnType > <FunctionName
>(){
…
return <
returnValue >;}
The only limitation here is that <
returnValue
>
must be a value that either is of type <
returnType
>
or can be implicitly converted to that type. However, <
returnType
>
can be any type you want, including the more complicated types you've seen. This might be as simple as the following:
static double GetVal()
{
return 3.2;
}
However, return values are usually the result of some processing carried out by the function; the preceding could be achieved just as easily using a const
variable.
When the return
statement is reached, program execution returns to the calling code immediately. No lines of code after this statement are executed, although this doesn't mean that return
statements can only be placed on the last line of a function body. You can use return
earlier in the code, perhaps after performing some branching logic. Placing return
in a for
loop, an if
block, or any other structure causes the structure to terminate immediately and the function to terminate:
static double GetVal()
{
double checkVal;
// checkVal assigned a value through some logic (not shown here).
if (checkVal < 5)
return 4.7;
return 3.2;
}
Here, one of two values is returned, depending on the value of checkVal
. The only restriction in this case is that a return
statement must be processed before reaching the closing }
of the function. The following is illegal:
static double GetVal()
{
double checkVal;
// checkVal assigned a value through some logic.
if (checkVal < 5)
return 4.7;
}
If checkVal
is >=
5
, then no return
statement is met, which isn't allowed. All processing paths must reach a return
statement. In most cases, the compiler detects this and gives you the error “not all code paths return a value.”
Functions that execute a single line of code can use a feature introduced in C# 6 called expression‐bodied methods. The following function pattern uses a =>
(lambda arrow) to implement this feature.
static <
returnType > <FunctionName
>() => <myVal1 * myVal2>;
For example, a Multiply()
function which prior to C# 6 is written like this:
static double Multiply(double myVal1, double myVal2)
{
return myVal1 * myVal2;
}
Can now be written using the =>
(lambda arrow). The result of the code written here expresses the intent of the method in a much simpler and consolidated way.
static double Multiply(double myVal1, double myVal2) => mVal1 * MyVal2;
When a function needs to accept parameters, you must specify the following:
Note that careful reading of the C# specification shows a subtle distinction between parameters and arguments. Parameters are defined as part of a function definition, whereas arguments are passed to a function by calling code. However, these terms are often used interchangeably, and nobody seems to get too upset about that.
This involves the following code, where you can have any number of parameters, each with a type and a name:
static <
returnType > <FunctionName
>(<paramType
> <paramName
>, …){
…
return <
returnValue >;}
The parameters are separated using commas, and each of these parameters is accessible from code within the function as a variable. For example, a simple function might take two double
parameters and return their product:
static double Product(double param1, double param2) => param1 * param2;
The following Try It Out provides a more complex example.
C:\BeginningCSharp7\Chapter06
.Program.cs
:class Program
{
static int MaxValue(int[] intArray)
{
int maxVal = intArray[0];
for (int i = 1; i < intArray.Length; i++)
{
if (intArray[i] > maxVal)
maxVal = intArray[i];
}
return maxVal;
}
static void Main(string[] args)
{
int[] myArray = { 1, 8, 3, 6, 2, 5, 9, 3, 0, 2 };
int maxVal = MaxValue(myArray);
WriteLine($"The maximum value in myArray is {maxVal}");
ReadKey();
}
}
This code contains a function that does what the example function at the beginning of this chapter hoped to do. It accepts an array of integers as a parameter and returns the highest number in the array. The function definition is as follows:
static int MaxValue(int[] intArray)
{
int maxVal = intArray[0];
for (int i = 1; i < intArray.Length; i++)
{
if (intArray[i] > maxVal)
maxVal = intArray[i];
}
return maxVal;
}
The function, MaxValue()
, has a single parameter defined, an int
array called intArray
. It also has a return type of int
. The calculation of the maximum value is simple. A local integer variable called maxVal
is initialized to the first value in the array, and then this value is compared with each of the subsequent elements in the array. If an element contains a higher value than maxVal
, then this value replaces the current value of maxVal
. When the loop finishes, maxVal
contains the highest value in the array, and is returned using the return
statement.
The code in Main()
declares and initializes a simple integer array to use with the MaxValue()
function:
int[] myArray = { 1, 8, 3, 6, 2, 5, 9, 3, 0, 2 };
The call to MaxValue()
is used to assign a value to the int
variable maxVal
:
int maxVal = MaxValue(myArray);
Next, you write that value to the screen using WriteLine()
:
WriteLine($"The maximum value in myArray is {maxVal}");
When you call a function, you must supply arguments that match the parameters as specified in the function definition. This means matching the parameter types, the number of parameters, and the order of the parameters. For example, the function
static void MyFunction(string myString, double myDouble)
{
…
}
can't be called using the following:
MyFunction(2.6, "Hello");
Here, you are attempting to pass a double
value as the first argument, and a string
value as the second argument, which is not the order in which the parameters are defined in the function definition. The code won't compile because the parameter type is wrong. In the “Overloading Functions” section later in this chapter, you'll learn a useful technique for getting around this problem.
C# enables you to specify one (and only one) special parameter for a function. This parameter, which must be the last parameter in the function definition, is known as a parameter array
. Parameter arrays enable you to call functions using a variable amount of parameters, and they are defined using the params
keyword.
Parameter arrays can be a useful way to simplify your code because you don't have to pass arrays from your calling code. Instead, you pass several arguments of the same type, which are placed in an array you can use from within your function.
The following code is required to define a function that uses a parameter array:
static <
returnType > <FunctionName
>(<p1Type
> <p1Name
>, …,params <
type >[] <name
>){
…
return <
returnValue >;}
You can call this function using code like the following:
<
FunctionName >(<p1
>, …, <val1
>, <val2
>, …)
<
val1
>
, <
val2
>
, and so on are values of type <
type
>
, which are used to initialize the
<name>
array. The number of arguments that you can specify here is almost limitless; the only restriction is that they must all be of type
<type>
. You can even specify no arguments at all.
The following Try It Out defines and uses a function with a params
type parameter.
C:\BeginningCSharp7\Chapter06
.Program.cs
:class Program
{
static int SumVals(params int[] vals)
{
int sum = 0;
foreach (int val in vals)
{
sum += val;
}
return sum;
}
static void Main(string[] args)
{
int sum = SumVals(1, 5, 2, 9, 8);
WriteLine($"Summed Values = {sum}");
ReadKey();
}
}
The function SumVals()
is defined using the params
keyword to accept any number of int
arguments (and no others):
static int SumVals(params int[] vals)
{
…
}
The code in this function simply iterates through the values in the vals
array and adds the values together, returning the result.
In Main()
, you call SumVals()
with five integer arguments:
int sum = SumVals(1, 5, 2, 9, 8);
You could just as easily call this function with none, one, two, or 100 integer arguments—there is no limit to the number you can specify.
C# includes alternative ways to specify function parameters, including a far more readable way to include optional parameters. You will learn about these methods in Chapter 13 , which looks at the C# language.
All the functions defined so far in this chapter have had value parameters. That is, when you have used parameters, you have passed a value into a variable used by the function. Any changes made to this variable in the function have no effect on the argument specified in the function call. For example, consider a function that doubles and displays the value of a passed parameter:
static void ShowDouble(int val)
{
val *= 2;
WriteLine($"val doubled = {val}");
}
Here, the parameter, val
, is doubled in this function. If you call it like this,
int myNumber = 5;
WriteLine($"myNumber = {myNumber}");
ShowDouble(myNumber);
WriteLine($"myNumber = {myNumber}");
then the text output to the console is as follows:
myNumber = 5
val doubled = 10
myNumber = 5
Calling ShowDouble()
with myNumber
as an argument doesn't affect the value of myNumber
in Main()
, even though the parameter it is assigned to, val
, is doubled.
That's all very well, but if you want
the value of myNumber
to change, you have a problem. You could use a function that returns a new value for myNumber
, like this:
static int DoubleNum(int val)
{
val *= 2;
return val;
}
You could call this function using the following:
int myNumber = 5;
WriteLine($"myNumber = {myNumber}");
myNumber = DoubleNum(myNumber);
WriteLine($"myNumber = {myNumber}");
However, this code is hardly intuitive and won't cope with changing the values of multiple variables used as arguments (as functions have only one return value).
Instead, you want to pass the parameter by reference
, which means that the function will work with exactly the same variable as the one used in the function call, not just a variable that has the same value. Any changes made to this variable will, therefore, be reflected in the value of the variable used as an argument. To do this, you simply use the ref
keyword to specify the parameter:
static void ShowDouble(ref int val)
{
val *= 2;
WriteLine($"val doubled = {val}");
}
Then, specify it again in the function call (this is mandatory):
int myNumber = 5;
WriteLine($"myNumber = {myNumber}");
ShowDouble(ref myNumber);
WriteLine($"myNumber = {myNumber}");
The text output to the console is now as follows:
myNumber = 5
val doubled = 10
myNumber = 10
Note two limitations on the variable used as a ref
parameter. First, the function might result in a change to the value of a reference parameter, so you must use a nonconstant
variable in the function call. The following is therefore illegal:
const int myNumber = 5;
WriteLine($"myNumber = {myNumber}");
ShowDouble(ref myNumber);
WriteLine($"myNumber = {myNumber}");
Second, you must use an initialized variable. C# doesn't allow you to assume that a ref
parameter will be initialized in the function that uses it. The following code is also illegal:
int myNumber;
ShowDouble(ref myNumber);
WriteLine("myNumber = {myNumber}");
Up to now you have seen the ref
keyword only applied to function parameters, but it is also possible to apply it to both local variables and returns. Here, myNumberRef
references mymyNumber
, and changing mymyNumberRef
results in a change to myNumber
. If the value of both myNumber
and myNumberRef
were displayed, the value would be 6 for both variables.
int myNumber = 5;
ref int myNumberRef = ref myNumber;
myNumberRef = 6;
It is also possible to use the ref
keyword as a return type. Notice in the following code the ref
keyword identifies the return type as ref int
, and is also in the code body, which instructs the function to return ref val
.
static
ref
int ShowDouble(int val){
val *= 2;
return
ref
val;}
If you attempted to compile the previous function you would receive an error. The reason is that you cannot pass a variable type as a function parameter by reference without prefixing the ref
keyword to the variable declaration. See the following code snippet where the ref
keyword is added—that function would compile and run as expected.
static ref int ShowDouble(
ref int val){
val *= 2;
return ref val;
}
Variables like strings
and arrays
are reference types and arrays
can be returned with the ref
keyword without a parameter declaration.
static ref int ReturnByRef()
{
int[] array = { 2 };
return ref array[0];
}
Although
strings
are reference types, they are a special case because they are immutable. That means you cannot change them because a modification results in a new
string
; the old
string
is deallocated. The C# compiler, Roslyn, will complain if you attempt to return a
string
by
ref
.
In addition to passing values by reference, you can specify that a given parameter is an out parameter
by using the out
keyword, which is used in the same way as the ref
keyword (as a modifier to the parameter in the function definition and in the function call). In effect, this gives you almost exactly the same behavior as a reference parameter, in that the value of the parameter at the end of the function execution is returned to the variable used in the function call. However, there are important differences:
ref
parameter, you can use an unassigned variable as an out
parameter.out
parameter must be treated as an unassigned value by the function that uses it.This means that while it is permissible in calling code to use an assigned variable as an out
parameter, the value stored in this variable is lost when the function executes.
As an example, consider an extension to the MaxValue()
function shown earlier, which returns the maximum value of an array. Modify the function slightly so that you obtain the index of the element with the maximum value within the array. To keep things simple, obtain just the index of the first occurrence of this value when there are multiple elements with the maximum value. To do this, you add an out
parameter by modifying the function as follows:
static int MaxValue(int[] intArray, out int maxIndex)
{
int maxVal = intArray[0];
maxIndex = 0;
for (int i = 1; i > intArray.Length; i++)
{
if (intArray[i] < maxVal)
{
maxVal = intArray[i];
maxIndex = i;
}
}
return maxVal;
}
You might use the function like this:
int[] myArray = { 1, 8, 3, 6, 2, 5, 9, 3, 0, 2 };
WriteLine("The maximum value in myArray is " +
$"{MaxValue(myArray, out int maxIndex)}");
WriteLine("The first occurrence of this value is " +
$" at element {maxIndex + 1}");
That results in the following:
The maximum value in myArray is 9
The first occurrence of this value is at element 7
You must use the out
keyword in the function call, just as with the ref
keyword. Another very useful situation for the out
keyword is when parsing data, for example:
if (!int.TryParse(input, out int result))
{
return null;
}
return result;
This code checks whether the value stored in the input
variable is an int
. If it is not of type int
, then the code snippet returns null
. If it is of type int
, then it returns the integer value via the out
variable declared as result
to the calling function.
There are numerous techniques for returning multiple values from a function. For example, you could use the out
keyword, structs, or an array, discussed previously, or via a class discussed later in this chapter. Using the out
keyword would achieve the goal of returning multiple values from a function; however, doing so uses that feature in a way it is not specifically designed to be used. Remember that the out
keyword is intended for passing a parameter by reference without needing to initialize it first. Structs, arrays, and classes are all valid options but require extra code to create, initialize, reference, and read. A tuple
, on the other hand, is a very elegant approach for achieving this objective with little overhead.
Because the tuple
provides a very convenient and direct approach to return more than a single value from a function, it's most useful when a program does not need a struct or more complicated implementations. Take this simple example of a tuple:
var numbers = (1, 2, 3, 4, 5);
That code creates a tuple
named numbers
containing members Item1
, Item2
, Item3
, Item4
, and Item5
which are accessible using, for example:
var number = numbers.Item1;
Or, to give the members a specific name, you can specifically identify them:
(int one, int two, int three, int four, int five) nums = (1, 2, 3, 4, 5);
int first = nums.one;
A method declaration would look something like the following:
private static (int max, int min, double average)
GetMaxMin(IEnumerable<int> numbers)
{
return (Enumerable.Max(numbers),
Enumerable.Min(numbers),
Enumerable.Average(numbers));
}
Then running the code from a simple console application using this code:
static void Main(string[] args)
{
IEnumerable<int> numbers = new int[] { 1, 2, 3, 4, 5, 6 };
var result = GetMaxMin(numbers);
WriteLine($"Max number is {result.max}, " +
$"Min number is {result.min}, " +
$"Average is {result.average}");
ReadLine();
}
results in the output shown in Figure 6‐4 .
In Chapter 10
, “Defining Classes Members,” where creating a classes and members is introduced, you will find an example of the deconstruction of a
tuple
. Understanding some basic class principles is required to grasp that concept completely, so read on and make note of the continuation of tuples in that chapter.
Throughout the last section, you might have been wondering why exchanging data with functions is necessary. The reason is that variables in C# are accessible only from localized regions of code. A given variable is said to have a scope from which it is accessible.
Variable scope is an important subject and one best introduced with an example. The following Try It Out illustrates a situation in which a variable is defined in one scope, and an attempt to use it is made in a different scope.
Program.cs
created previously:class Program
{
static void Write()
{
WriteLine($"myString = {myString}");
}
static void Main(string[] args)
{
string myString = "String defined in Main()";
Write();
ReadKey();
}
}
The name 'myString' does not exist in the current context
The variable 'myString' is assigned but its value is never used
What went wrong? Well, the variable myString
defined in the main body of your application (the Main()
function) isn't accessible from the Write()
function.
The reason for this inaccessibility is that variables have a scope within which they are valid. This scope encompasses the code block that they are defined in and any directly nested code blocks. The blocks of code in functions are separate from the blocks of code from which they are called. Inside Write()
, the name myString
is undefined, and the myString
variable defined in Main()
is out of scope
—it can be used only from within Main()
.
In fact, you can have a completely separate variable in Write()
called myString
. Try modifying the code as follows:
class Program
{
static void Write()
{
string myString = "String defined in Write()";
WriteLine("Now in Write()");
WriteLine($"myString = {myString}");
}
static void Main(string[] args)
{
string myString = "String defined in Main()";
Write();
WriteLine("\nNow in Main()");
WriteLine($"myString = {myString}");
ReadKey();
}
}
This code does compile, resulting in the output shown in Figure 6‐5 .
The operations performed by this code are as follows:
Main()
defines and initializes a string variable called myString
.Main()
transfers control to Write()
.Write()
defines and initializes a string variable called myString
, which is a different variable from the myString
defined in Main()
.Write()
outputs a string to the console containing the value of myString
as defined in Write()
.Write()
transfers control back to Main()
.Main()
outputs a string to the console containing the value of myString
as defined in Main()
.Variables whose scopes cover a single function in this way are known as local variables . It is also possible to have global variables , whose scopes cover multiple functions. Modify the code as follows:
class Program
{
static string myString;
static void Write()
{
string myString = "String defined in Write()";
WriteLine("Now in Write()");
WriteLine($"Local myString = {myString}");
WriteLine($"Global myString = {Program.myString}");
}
static void Main(string[] args)
{
string myString = "String defined in Main()";
Program.myString = "Global string";
Write();
WriteLine("\nNow in Main()");
WriteLine($"Local myString = {myString}");
WriteLine($"Global myString = {Program.myString}");
ReadKey();
}
}
The result is now as shown in Figure 6‐6 .
Here, you have added another variable called myString
, this time further up the hierarchy of names in the code. The variable is defined as follows:
static string myString;
Again, the static
keyword is required. Without going into too much detail, understand that in this type of console application, you must use either the static
or the const
keyword for global variables
of this form. If you want to modify the value of the global variable, you need to use static
because const
prohibits the value of the variable from changing.
To differentiate between this variable and the local variables in Main()
and Write()
with the same names, you have to classify the variable name using a fully qualified name, as described in Chapter 3
. Here, you refer to the global version as Program.myString
. This is necessary only when you have global and local variables with the same name; if there were no local myString
variable, you could simply use myString
to refer to the global variable, rather than Program.myString
. When you have a local variable with the same name as a global variable, the global variable is said to be hidden.
The value of the global variable is set in Main()
with
Program.myString = "Global string";
and accessed in Write()
with
WriteLine($"Global myString = {Program.myString}");
You might be wondering why you shouldn't just use this technique to exchange data with functions, rather than the parameter passing shown earlier. There are indeed situations where this is an acceptable way to exchange data, for example if you are writing a single object to be used as a plugin or a short script for use in a larger project. However, there are many scenarios where it isn't a good idea. The most common issue with using global variables has to do with the management of concurrency. For example, a global variable can be written to and read from numerous methods within a class or from different threads. Can you be certain that the value in the global variable contains valid data if numerous threads and methods can write to it? Without some extra synchronization code, the answer is probably not. Additionally, over time it is possible the actual intent of the global variable is forgotten and used later for some other reason. Therefore, the choice of whether to use global variables depends on the intended use of the function in question.
The problem with using global variables is that they are generally unsuitable for “general‐purpose” functions, which can work with whatever data you supply, not just data in a specific global variable. You look at this in more depth a little later.
One of the points made in the last section has consequences above and beyond variable scope between functions: that the scopes of variables encompass the code blocks in which they are defined and any directly nested code blocks. You can find the code discussed next in the chapter download in VariableScopeInLoops\Program.cs
. This also applies to other code blocks, such as those in branching and looping structures. Consider the following code:
int i;
for (i = 0; i < 10; i++)
{
string text = $"Line {Convert.ToString(i)}";
WriteLine($"{text}");
}
WriteLine($"Last text output in loop: {text}");
Here, the string variable text
is local to the for
loop. This code won't compile because the call to WriteLine()
that occurs outside of this loop attempts to use the variable text
, which is out of scope outside of the loop. Try modifying the code as follows:
int i;
string text;
for (i = 0; i < 10; i++)
{
text = $"Line {Convert.ToString(i)}";
WriteLine($"{text}");
}
WriteLine($"Last text output in loop: {text}");
This code will also fail because variables must be declared and initialized before use, and text
is only initialized in the for
loop. The value assigned to text
is lost when the loop block is exited as it isn't initialized outside the block. However, you can make the following change:
int i;
string text = "";
for (i = 0; i < 10; i++)
{
text = $"Line {Convert.ToString(i)}";
WriteLine($"{text}");
}
WriteLine($"Last text output in loop: {text}");
This time text
is initialized outside of the loop, and you have access to its value. The result of this simple code is shown in Figure 6‐7
.
The last value assigned to text
in the loop is accessible from outside the loop. As you can see, this topic requires a bit of effort to come to grips with. It is not immediately obvious why, in light of the earlier example, text
doesn't retain the empty string it is assigned before the loop in the code after the loop.
The explanation for this behavior is related to memory allocation for the text
variable, and indeed any variable. Merely declaring a simple variable type doesn't result in very much happening. It is only when values are assigned to the variables that values are allocated a place in memory to be
stored. When this allocation takes place inside a loop, the value is essentially defined as a local value and goes out of scope outside of the loop.
Even though the variable itself isn't localized to the loop, the value it contains is. However, assigning a value outside of the loop ensures that the value is local to the main code, and is still in scope inside the loop. This means that the variable doesn't go out of scope before the main code block is exited, so you have access to its value outside of the loop.
Luckily for you, the C# compiler detects variable scope problems, and responding to the error messages it generates certainly helps you to understand the topic of variable scope.
Let's take a closer look at exchanging data with functions via global data and via parameters and return values. To recap, consider the following code:
class Program
{
static void ShowDouble(ref int val)
{
val *= 2;
WriteLine($"val doubled = {val}");
}
static void Main(string[] args)
{
int val = 5;
WriteLine($"val = {val}");
ShowDouble(ref val);
WriteLine($"val = {val}");
}
}
This code is slightly different from the code shown earlier in this chapter when you used the variable name
myNumber
in
Main()
. This illustrates the fact that local variables can have identical names and yet not interfere with each other.
Now compare it with this code:
class Program
{
static int val;
static void ShowDouble()
{
val *= 2;
WriteLine($"val doubled = {val}");
}
static void Main(string[] args)
{
val = 5;
WriteLine($"val = {val}");
ShowDouble();
WriteLine($"val = {val}");
}
}
The results of these ShowDouble()
functions are identical.
There are no hard‐and‐fast rules for using one technique rather than another, and both techniques are perfectly valid, but you might want to consider the following guidelines.
To start with, as mentioned when this topic was first introduced, the ShowDouble()
version that uses the global value only uses the global variable val
. To use this version, you must use this global variable. This limits the versatility of the function slightly and means that you must continuously copy the global variable value into other variables if you intend to store the results. In addition, global data might be modified by code elsewhere in your application, which could cause unpredictable results (values might change without you realizing it until it's too late).
Of course, it could also be argued that this simplicity actually makes your code more difficult to understand. Explicitly specifying parameters enables you to see at a glance what is changing. If you see a call that reads FunctionName(val1,
out
val2)
, you instantly know that val1
and val2
are the important variables to consider and that val2
will be assigned a new value when the function is completed. Conversely, if this function took no parameters, then you would be unable to make any assumptions about what data it manipulated.
Feel free to use either technique to exchange data. In general, use parameters rather than global data; however, there are certainly cases where global data might be more suitable, and it certainly isn't an error to use that technique.
As mentioned at the beginning of this chapter where the concept of a function was introduced, the point was made that the reason for taking code out of the Main(string[] args)
function is so that it can be reused instead of recoded multiple times within the same program. We want to emphasize that you should abide by that way of thinking when you design and create your programs in most situations.
Keep in mind that over time, the complexity of a program can significantly increase via an expansion of what it is expected to do. As the capabilities of the program increase, it is likely that many more functions will be added to to enable them. The more functions a program has, the more difficult it is for other programmers to make changes such as fixing bugs or adding new features. Making changes becomes harder not only due to the number of functions, but also because the original intent of the functions can get lost. Such functions then could be used for reasons other than those the original author intended, which can cause serious problems when changes get wrongly made to them.
If you ever find yourself needing to modify functions you didn't write, consider a local function instead. Local functions allow you to declare a function within the context of another function. Doing this can help readability and speed interpretation of the program's purpose.
Take this code for example:
class Program
{
static void Main(string[] args)
{
int myNumber = 5;
WriteLine($"Main Function = {myNumber}");
DoubleIt(myNumber);
ReadLine();
void DoubleIt(int val)
{
val *= 2;
WriteLine($"Local Function - val = {val}");
}
}
}
Notice that the function DoubleIt()
exists within the Main(string[] args)
function. It cannot be called from other functions contained in the Program
class. The result of this simple code is shown in Figure 6‐8
.
Finally, keep in mind that you can write an asynchronous local function by placing the keyword async
in front of the function declaration. Asynchronous programming is an advanced topic which is not covered in this book; however, it is important for you to know that the capability exists.
Now that you've covered most of the simple techniques used in the creation and use of functions, it's time to take a closer look at the Main()
function.
Earlier, you saw that Main()
is the entry point for a C# application and that execution of this function encompasses the execution of the application. That is, when execution is initiated, the Main()
function executes, and when the Main()
function finishes, execution ends.
The Main()
function can return either void
or int
, and can optionally include a string[]
args
parameter, so you can use any of the following versions:
static void Main()
static void Main(string[] args)
static int Main()
static int Main(string[] args)
The third and fourth versions return an int
value, which can be used to signify how the application terminates, and often is used as an indication of an error (although this is by no means mandatory). In general, returning a value of 0 reflects normal termination (that is, the application has completed and can terminate safely).
The optional args
parameter of Main()
provides you with a way to obtain information from outside the application, specified at runtime. This information takes the form of command‐line parameters
.
When a console application is executed, any specified command‐line parameters are placed in this args
array. You can then use these parameters in your application. The following Try It Out shows this in action. You can specify any number of command‐line arguments, each of which will be output to the console.
C:\BeginningCSharp7\Chapter06
.Program.cs
:class Program
{
static void Main(string[] args)
{
WriteLine($"{args.Length} command line arguments were specified:");
foreach (string arg in args)
WriteLine(arg);
ReadKey();
}
}
The code used here is very simple:
WriteLine($"{args.Length} command line arguments were specified:");
foreach (string arg in args)
WriteLine(arg);
You're just using the args
parameter as you would any other string array. You're not doing anything fancy with the arguments; you're just writing whatever is specified to the screen. You supplied the arguments via the project properties in the IDE. This is a handy way to use the same command‐line arguments whenever you run the application from the IDE, rather than type them at a command‐line prompt every time. The
same result can be obtained by opening a command prompt window in the same directory as the project output (C:\
BeginningCSharp7\Chapter06\Ch06Ex04\Ch06Ex04\bin\Debug
) and typing this:
Ch06Ex04 256 myFile.txt "a longer argument"
Each argument is separated from the next by spaces. To supply an argument that includes spaces, you can enclose it in double quotation marks, which prevents it from being interpreted as multiple arguments.
The last chapter covered struct types for storing multiple data elements in one place. Structs are actually capable of a lot more than this. For example, they can contain functions as well as data. That might seem a little strange at first, but it is, in fact, very useful. As a simple example, consider the following struct:
struct CustomerName
{
public string firstName, lastName;
}
If you have variables of type CustomerName
and you want to output a full name to the console, you are forced to build the name from its component parts. You might use the following syntax for a CustomerName
variable called myCustomer
, for example:
CustomerName myCustomer;
myCustomer.firstName = "John";
myCustomer.lastName = "Franklin";
WriteLine($"{myCustomer.firstName} {myCustomer.lastName}");
By adding functions to structs, you can simplify this by centralizing the processing of common tasks. For example, you can add a suitable function to the struct type as follows:
struct CustomerName
{
public string firstName, lastName;
public string Name() => firstName + " " + lastName;
}
This looks much like any other function you've seen in this chapter, except that you haven't used the static
modifier. The reasons for this will become clear later in the book; for now, it is enough to know that this keyword isn't required for struct functions. You can use this function as follows:
CustomerName myCustomer;
myCustomer.firstName = "John";
myCustomer.lastName = "Franklin";
WriteLine(myCustomer.Name());
This syntax is much simpler, and much easier to understand, than the previous syntax. The Name()
function has direct access to the firstName
and lastName
struct members. Within the customerName
struct, they can be thought of as global.
Earlier in this chapter, you saw how you must match the signature of a function when you call it. This implies that you need to have separate functions to operate on different types of variables. Function overloading provides you with the capability to create multiple functions with the same name, but each working with different parameter types. For example, earlier you used the following code, which contains a function called MaxValue()
:
class Program
{
static int MaxValue(int[] intArray)
{
int maxVal = intArray[0];
for (int i = 1; i < intArray.Length; i++)
{
if (intArray[i] > maxVal)
maxVal = intArray[i];
}
return maxVal;
}
static void Main(string[] args)
{
int[] myArray = { 1, 8, 3, 6, 2, 5, 9, 3, 0, 2 };
int maxVal = MaxValue(myArray);
WriteLine("The maximum value in myArray is {maxVal}");
ReadKey();
}
}
This function can be used only with arrays of int
values. You could provide different named functions for different parameter types, perhaps renaming the preceding function as IntArrayMaxValue()
and adding functions such as DoubleArrayMaxValue()
to work with other types. Alternatively, you could just add the following function to your code:
static double MaxValue(double[] doubleArray)
{
double maxVal = doubleArray[0];
for (int i = 1; i < doubleArray.Length; i++)
{
if (doubleArray[i] > maxVal)
maxVal = doubleArray[i];
}
return maxVal;
}
The difference here is that you are using double
values. The function name, MaxValue()
, is the same, but (crucially) its signature
is different. That's because the signature of a function, as shown
earlier, includes both the name of the function and its parameters. It would be an error to define two functions with the same signature, but because these two functions have different signatures, this is fine.
The return type of a function isn't part of its signature, so you can't define two functions that differ only in return type; they would have identical signatures.
After adding the preceding code, you have two versions of MaxValue()
, which accept int
and double
arrays, returning an int
or double
maximum, respectively.
The beauty of this type of code is that you don't have to explicitly specify which of these two functions you want to use. You simply provide an array parameter, and the correct function is executed depending on the type of parameter used.
Note another aspect of the IntelliSense feature in Visual Studio: When you have the two functions shown previously in an application and then proceed to type the name of the function, for example, Main()
, the IDE shows you the available overloads for that function. For example, if you type
double result = MaxValue(
the IDE gives you information about both versions of MaxValue()
, which you can scroll between using the Up and Down arrow keys, as shown in Figure 6‐11
.
All aspects of the function signature are included when overloading functions. You might, for example, have two different functions that take parameters by value and by reference, respectively:
static void ShowDouble(ref int val)
{
…
}
static void ShowDouble(int val)
{
…
}
Deciding which version to use is based purely on whether the function call contains the ref
keyword. The following would call the reference version:
ShowDouble(ref val);
This would call the value version:
ShowDouble(val);
Alternatively, you could have functions that differ in the number of parameters they require, and so on.
A delegate
is a type that enables you to store references to functions. Although this sounds quite involved, the mechanism is surprisingly simple. The most important purpose of delegates will become clear later in the book when you look at events and event handling, but it's useful to briefly consider them here. Delegates are declared much like functions, but with no function body and using the delegate
keyword. The delegate declaration specifies a return type and parameter list.
After defining a delegate, you can declare a variable with the type of that delegate. You can then initialize the variable as a reference to any function that has the same return type and parameter list as that delegate. Once you have done this, you can call that function by using the delegate variable as if it were a function.
When you have a variable that refers to a function, you can also perform other operations that would be otherwise impossible. For example, you can pass a delegate variable to a function as a parameter, and then that function can use the delegate to call whatever function it refers to, without knowing which function will be called until runtime. The following Try It Out demonstrates using a delegate to access one of two functions.
C:\BeginningCSharp7\Chapter06
.Program.cs
:class Program
{
delegate double ProcessDelegate(double param1, double param2);
static double Multiply(double param1, double param2) => param1 * param2;
static double Divide(double param1, double param2) => param1 / param2;
static void Main(string[] args)
{
ProcessDelegate process;
WriteLine("Enter 2 numbers separated with a comma:");
string input = ReadLine();
int commaPos = input.IndexOf(',');
double param1 = ToDouble(input.Substring(0, commaPos));
double param2 = ToDouble(input.Substring(commaPos + 1,
input.Length - commaPos - 1));
WriteLine("Enter M to multiply or D to divide:");
input = ReadLine();
if (input == "M")
process = new ProcessDelegate(Multiply);
else
process = new ProcessDelegate(Divide);
WriteLine($"Result: {process(param1, param2)}");
ReadKey();
}
}
This code defines a delegate (ProcessDelegate
) whose return type and parameters match those of the two functions (Multiply()
and Divide()
). Notice that the Multiply()
and Divide()
functions use the => (lambda arrow / expression‐bodied methods).
static double Multiply(double param1, double param2) => param1 * param2;
The delegate definition is as follows:
delegate double ProcessDelegate(double param1, double param2);
The delegate
keyword specifies that the definition is for a delegate, rather than a function (the definition appears in the same place that a function definition might). Next, the definition specifies a double
return value and two double
parameters. The actual names used are arbitrary; you can call the delegate type and parameter names whatever you like. This example uses a delegate called ProcessDelegate
and double parameters called param1
and param2
.
The code in Main()
starts by declaring a variable using the new delegate type:
static void Main(string[] args)
{
ProcessDelegate process;
Next, you have some fairly standard C# code that requests two numbers separated by a comma, and then places these numbers in two double
variables:
WriteLine("Enter 2 numbers separated with a comma:");
string input = ReadLine();
int commaPos = input.IndexOf(',');
double param1 = ToDouble(input.Substring(0, commaPos));
double param2 = ToDouble(input.Substring(commaPos + 1,
input.Length - commaPos - 1));
For demonstration purposes, no user input validation is included here. If this were “real” code, you'd spend much more time ensuring that you had valid values in the local
param1
and
param2
variables.
Next, you ask the user to multiply or divide these numbers:
WriteLine("Enter M to multiply or D to divide:");
input = ReadLine();
Based on the user's choice, you initialize the process
delegate variable:
if (input == "M")
process = new ProcessDelegate(Multiply);
else
process = new ProcessDelegate(Divide);
To assign a function reference to a delegate variable, you use slightly odd‐looking syntax. Much like assigning array values, you can use the new
keyword to create a new delegate. After this keyword, you specify the delegate type and supply an argument referring to the function you want to use—namely, the Multiply()
or Divide()
function. This argument doesn't match the parameters of the delegate type or the target function; it is a syntax unique to delegate assignment. The argument is simply the name of the function to use, without any parentheses.
In fact, you can use slightly simpler syntax here, if you want:
if (input == "M")
process = Multiply;
else
process = Divide;
The compiler recognizes that the delegate type of the process variable matches the signature of the two functions, and automatically initializes a delegate for you. Which syntax you use is up to you, although some people prefer to use the longhand version, as it is easier to see at a glance what is happening.
Finally, call the chosen function using the delegate. The same syntax works, regardless of which function the delegate refers to:
WriteLine($"Result: {process(param1, param2)}");
ReadKey();
}
Here, you treat the delegate variable as if it were a function name. Unlike a function, though, you can also perform additional operations on this variable, such as passing it to a function via a parameter, as shown in this simple example:
static void ExecuteFunction(ProcessDelegate process)
=> process(2.2, 3.3);
This means that you can control the behavior of functions by passing them function delegates, much like choosing a “snap‐in” to use. For example, you might have a function that sorts a string array alphabetically. You can use several techniques to sort lists, with varying performance depending on the characteristics of the list being sorted. By using delegates, you can specify the function to use by passing a sorting algorithm function delegate to a sorting function.
There are many such uses for delegates, but, as mentioned earlier, their most prolific use is in event handling, covered in Chapter 13 .
static bool Write()
{
WriteLine("Text output from function.");
}
static void MyFunction(string label, params int[] args, bool showLabel)
{
if (showLabel)
WriteLine(label);
foreach (int i in args)
WriteLine($"{i}");
}
ReadLine()
function when asking for user input.struct order
{
public string itemName;
public int unitCount;
public double unitCost;
}
Order Information: <
unit count > <item name
> items at $<unit cost
> each,total cost $<
total cost >
Answers to the exercises can be found in Appendix.
TOPIC | KEY CONCEPTS |
Defining functions | Functions are defined with a name, zero or more parameters, and a return type. The name and parameters of a function collectively define the signature of the function. It is possible to define multiple functions whose signatures are different even though their names are the same—this is called function overloading . Functions can also be defined within struct types. |
Return values and parameters | The return type of a function can be any type, or void
if the function does not return a value. Parameters can also be of any type, and consist of a comma‐separated list of type and name pairs. A variable number of parameters of a specified type can be specified through a parameter array. Parameters can be specified as ref
or out
parameters in order to return values to the caller. When calling a function, any arguments specified must match the parameters in the definition both in type and in order and must include matching ref
and out
keywords if these are used in the parameter definition. |
Variable scope | Variables are scoped according to the block of code where they are defined. Blocks of code include methods as well as other structures, such as the body of a loop. It is possible to define multiple, separate variables with the same name at different scope levels. |
Command‐line parameters | The Main()
function in a console application can receive command‐line parameters that are passed to the application when it is executed. When executing the application, these parameters are specified by arguments separated by spaces, and longer arguments can be passed in quotes. |
Delegates | As well as calling functions directly, it is possible to call them through delegates. Delegates are variables that are defined with a return type and parameter list. A given delegate type can match any method whose return type and parameters match the delegate definition. |