C# 7.0 introduced the ability to declare functions within methods. In other words, rather than always declaring a method within a class, the method (technically a function) could be declared within another method (see Listing 5.7).
This language construct is called a local function. The reason to declare it like this is that the function’s scope is limited to invocation from within the method where it is declared. Outside of the method’s scope, it is not possible to call the local function. In addition, the local function can access local variables in the method that are declared before the local function. Alternatively, this can explicitly be prevented by adding the static to the beginning of the local function declaration (see “Static Anonymous Functions” in Chapter 13).
Fully qualified namespace names can become quite long and unwieldy. It is possible, however, to import all the types from one or more namespaces so that they can be used without full qualification.
Rather than a declarative that applies to the entire project, C# includes a using directive that applies only to the current file. For example, Listing 5.8 (with Output 5.2) doesn’t prefix RegEx with System.Text.RegularExpressions. The namespace may be omitted because of the using System.Text.RegularExpressions directive that appears at the top of the listing.
A using directive such as using System.Text does not enable you to omit System.Text from a type declared within a nested namespace such as System.Text.RegularExpressions. For example, if your code accessed the RegEx type from the System.Text.RegularExpressions namespace, you would have to either include an additional using System.Text.RegularExpressions directive or fully qualify the type as System.Text.RegularExpressions.RegEx, not just RegularExpressions.RegEx. In short, a using directive does not import types from any nested namespaces. Nested namespaces, which are identified by the period in the namespace, always need to be imported explicitly.
Java enables importing namespaces using a wildcard such as the following:
In contrast, C# does not support a wildcard using directive but instead requires each namespace to be imported explicitly.
Frequent use of types within a particular namespace implies that the addition of a using directive for that namespace is a good idea, instead of fully qualifying all types within the namespace. Accordingly, almost all C# files include the using System directive at the top. Throughout the remainder of this book, code listings often omit the using System directive from the manuscript. Other namespace directives are generally included explicitly, however.
One interesting effect of the using System directive is that the string data type can be identified with varying case: String or string. The former version relies on the using System directive, and the latter uses the string keyword. Both are valid C# references to the System.String data type, and the resultant Common Intermediate Language (CIL) code is unaffected by which version is chosen.
Readers will recall from Chapter 1 there was an ImplicitUsings element within the .csproj file for C# 10.0 and later:
In addition, the source code consistently refers to things like Console, even though the fully qualified name is System.Console. The ability to abbreviate is afforded by the ImplicitUsings element, which tells the compiler to infer the namespace automatically rather than require the programmer to provide it. Using directives, therefore, allows the use of type identifiers without specifying their fully qualified name. To accomplish this, the complier generates global using directives whenever the ImplicitUsings element is set to enable.
If you search the subdirectory of a C# 10.0 (or higher) project, you will notice there is a file with the extension .GlobalUsing.g.cs, generally found in an obj folder subdirectory, and it contains multiple global using directives such as those found in Listing 5.9.
Each of these lines4 is a global using directive that tells the compiler to imply the namespace qualifier for any type that appears within the namespace specified. For example, global using global::System allows implicit using directives like Console.WriteLine("Hello! My name is Inigo Montoya!") rather than the fully qualified name System.Console.WriteLine(...) because Console is defined in the System namespace.
Of course, you can provide your own global using declaratives. Say, for example, you frequently are using the System.Text.StringBuilder class (initially introduced in Chapter 2). Instead of always referring to it by the fully qualified name, you can provide a global using directive for the System.Text namespace and then just refer to the type using the abbreviation StringBuilder (see Listing 5.10), also still relying on implicit usings for System.Console.
The global using directives applies to the entire project regardless of in which file it appears. And, although C# allows the same global using declarative multiple times, doing so is redundant. For this reason, it is preferable to place all the global using directives into a single file named Usings.cs by convention. Collocating them here also provides a well-known location to place them or remove such declaratives.
While global using directives apply to the entire project, C# also supports using directives that apply only to the current file. The global using declarative, however, must appear before any other (non-global) using directives, which we cover after the .csproj using element.
It is also possible to specify global using declaratives within your project (.csproj) file with a Using element, as shown in Listing 5.11
The important thing to note is that unlike the ImplicitUsings element, the Using element is a subelement to the ItemGroup element rather than the PropertyGroup element. And, given the Using element for System.Net specified, it is no longer necessary to fully qualify HttpClient, which is part of the System.Net namespace. Instead, the compiler will generate a global using System.Net directive when building the project. There is also another Using statement that includes a Static attribute on it, which we will explain further in the next section.
The using directive allows you to abbreviate a type name by omitting the namespace portion of the name—such that just the type name can be specified for any type within the stated namespace. In contrast, the using static directive allows you to omit both the namespace and the type name from any static member of the stated type. A using static System.Console directive, for example, allows you to specify WriteLine() rather than the fully qualified method name of System.Console.WriteLine(). Continuing with this example, we can update Listing 5.2 to leverage the using static System.Console directive to create Listing 5.13.
In this case, there is no loss of readability of the code: WriteLine(), Write(), and ReadLine() all clearly relate to a console directive. In fact, one could argue that the resulting code is simpler and therefore clearer than before.
However, sometimes this is not the case. For example, if your code uses classes that have overlapping behavior names, such as an Exists() method on a file and an Exists() method on a directory, then a using static directive would reduce clarity when you invoke Exists(). Similarly, if the class you were writing had its own members with overlapping behavior names—for example, Display() and Write()—then clarity would be lost to the reader.
This ambiguity would not be allowed by the compiler. If two members with the same signature were available (through either using static directives or separately declared members), any invocation of them that was ambiguous would result in a compile error.
Note that C# 10 or later also support global static using directives such as:
global using static System.Console;
Similarly, a global using static directive can be configured in the csproj file:
<Using Static="true" Include="System.Console"/>
This is also shown in Listing 5.11.
Starting in C# 12.0, the using directive also allows aliasing a namespace or any type including tuples, pointers (Chapter 23), array types, and generic types (Chapter 12). An alias is an alternative name that you can use within the text to which the using directive applies. The two most common reasons for aliasing are to disambiguate two types that have the same name and to abbreviate a long name. In Listing 5.14, for example, the CountDownTimer alias is declared as a means of referring to the type System.Timers.Timer. Simply adding a using System.Timers directive will not sufficiently enable the code to avoid fully qualifying the Timer type. The reason is that System.Threading also includes a type called Timer; therefore, using just Timer within the code will be ambiguous.
Listing 5.14 uses an entirely new name, CountDownTimer, as the alias. It is possible, however, to specify the alias as Timer, as shown in Listing 5.15.
Because of the alias directive, “Timer” is not an ambiguous reference. Furthermore, to refer to the System.Threading.Timer type, you will have to either qualify the type or define a different alias.
Of course, C# 10 and later also support global aliasing using directives such as:
global using Timer = Timers.Timer;
And like with all other global using directives, a global using alias directive can be configured in the csproj file:
<Using Include=" System.Timers.Timer" Alias="Timer"/>