Referenced Before Assignment In Enclosing Scope Scholastic

The latest version of this topic can be found at Lambda Expressions in C++.

In C++11, a lambda expression—often called a lambda—is a convenient way of defining an anonymous function object right at the location where it is invoked or passed as an argument to a function. Typically lambdas are used to encapsulate a few lines of code that are passed to algorithms or asynchronous methods. This article defines what lambdas are, compares them to other programming techniques, describes their advantages, and provides a basic example.

The ISO C++ Standard shows a simple lambda that is passed as the third argument to the function:

This illustration shows the parts of a lambda:

  1. capture clause (Also known as the lambda-introducer in the C++ specification.)

  2. parameter list Optional. (Also known as the lambda declarator)

  3. mutable specification Optional.

  4. exception-specification Optional.

  5. trailing-return-type Optional.

  6. lambda body)

Capture Clause

A lambda can introduce new variables in its body (in C++14), and it can also access—or capture--variables from the surrounding scope. A lambda begins with the capture clause (lambda-introducer in the Standard syntax), which specifies which variables are captured, and whether the capture is by value or by reference. Variables that have the ampersand () prefix are accessed by reference and variables that do not have it are accessed by value.

An empty capture clause, , indicates that the body of the lambda expression accesses no variables in the enclosing scope.

You can use the default capture mode ( in the Standard syntax) to indicate how to capture any outside variables that are referenced in the lambda: [&] means all variables that you refer to are captured by reference, and [=] means they are captured by value. You can use a default capture mode, and then specify the opposite mode explicitly for specific variables. For example, if a lambda body accesses the external variable by reference and the external variable by value, then the following capture clauses are equivalent:

Only variables that are mentioned in the lambda are captured when a is used.

If a capture clause includes a , then no in a of that capture clause can have the form . Likewise, if the capture clause includes a , then no of that capture clause can have the form . An identifier or cannot appear more than once in a capture clause. The following code snippet illustrates some examples.

A followed by an ellipsis is a pack expansion, as shown in this variadic template example:

To use lambda expressions in the body of a class method, pass the pointer to the capture clause to provide access to the methods and data members of the enclosing class. For an example that shows how to use lambda expressions with class methods, see "Example: Using a Lambda Expression in a Method" in Examples of Lambda Expressions.

When you use the capture clause, we recommend that you keep these points in mind, particularly when you use lambdas with multithreading:

  • Reference captures can be used to modify variables outside, but value captures cannot. ( allows copies to be modified, but not originals.)

  • Reference captures reflect updates to variables outside, but value captures do not.

  • Reference captures introduce a lifetime dependency, but value captures have no lifetime dependencies. This is especially important when the lambda runs asynchronously. If you capture a local by reference in an async lambda, that local will very possibly be gone by the time the lambda runs, resulting in an access violation at run time.

Generalized capture (C++ 14)

In C++14, you can introduce and initialize new variables in the capture clause, without the need to have those variables exist in the lambda function’s enclosing scope. The initialization can be expressed as any arbitrary expression; the type of the new variable is deduced from the type produced by the expression. One benefit of this feature is that in C++14 you can capture move-only variables (such as std::unique_ptr) from the surrounding scope and use them in a lambda.

Parameter List

In addition to capturing variables, a lambda can accept input parameters. A parameter list (lambda declarator in the Standard syntax) is optional and in most aspects resembles the parameter list for a function.

In C++ 14, if the parameter type is generic, you can use the auto keyword as the type specifier. This tells the compiler to create the function call operator as a template. Each instance of auto in a parameter list is equivalent to a distinct type parameter.

A lambda expression can take another lambda expression as its argument. For more information, see "Higher-Order Lambda Expressions" in the topic Examples of Lambda Expressions.

Because a parameter list is optional, you can omit the empty parentheses if you do not pass arguments to the lambda expression and its does not contain exception-specification, trailing-return-type, or .

Mutable Specification

Typically, a lambda's function call operator is const-by-value, but use of the keyword cancels this out. It does not produce mutable data members. The mutable specification enables the body of a lambda expression to modify variables that are captured by value. Some of the examples later in this article show how to use .

Exception Specification

You can use the exception specification to indicate that the lambda expression does not throw any exceptions. As with ordinary functions, the Visual C++ compiler generates warning C4297 if a lambda expression declares the exception specification and the lambda body throws an exception, as shown here:

For more information, see Exception Specifications (throw).

Return Type

The return type of a lambda expression is automatically deduced. You don't have to use the auto keyword unless you specify a trailing-return-type. The trailing-return-type resembles the return-type part of an ordinary method or function. However, the return type must follow the parameter list, and you must include the trailing-return-type keyword before the return type.

You can omit the return-type part of a lambda expression if the lambda body contains just one return statement or the expression does not return a value. If the lambda body contains one return statement, the compiler deduces the return type from the type of the return expression. Otherwise, the compiler deduces the return type to be . Consider the following example code snippets that illustrate this principle.

A lambda expression can produce another lambda expression as its return value. For more information, see "Higher-Order Lambda Expressions" in Examples of Lambda Expressions.

Lambda Body

The lambda body (compound-statement in the Standard syntax) of a lambda expression can contain anything that the body of an ordinary method or function can contain. The body of both an ordinary function and a lambda expression can access these kinds of variables:

  • Captured variables from the enclosing scope, as described previously.

  • Parameters

  • Locally-declared variables

  • Class data members, when declared inside a class and is captured

  • Any variable that has static storage duration—for example, global variables

The following example contains a lambda expression that explicitly captures the variable by value and implicitly captures the variable by reference:

Output:

Because the variable is captured by value, its value remains after the call to the lambda expression. The specification allows to be modified within the lambda.

Although a lambda expression can only capture variables that have automatic storage duration, you can use variables that have static storage duration in the body of a lambda expression. The following example uses the function and a lambda expression to assign a value to each element in a object. The lambda expression modifies the static variable to generate the value of the next element.

For more information, see generate.

The following code example uses the function from the previous example, and adds an example of a lambda expression that uses the STL algorithm . This lambda expression assigns an element of a object to the sum of the previous two elements. The keyword is used so that the body of the lambda expression can modify its copies of the external variables and , which the lambda expression captures by value. Because the lambda expression captures the original variables and by value, their values remain after the lambda executes.

Output:

For more information, see generate_n.

Lambdas are not supported in the following common language runtime (CLR) managed entities: , , , or .

If you are using a Microsoft-specific modifier such as __declspec, you can insert it into a lambda expression immediately after the —for example:

To determine whether a modifier is supported by lambdas, see the article about it in the Microsoft-Specific Modifiers section of the documentation.

Visual Studio supports C++11 Standard lambda expression syntax and functionality, with these exceptions:

  • Like all other classes, lambdas don't get automatically generated move constructors and move assignment operators. For more information about support for rvalue reference behaviors, see the "Rvalue References" section in Support For C++11/14/17 Features (Modern C++).

  • The optional attribute-specifier-seq is not supported in this version.

Visual Studio includes these features in addition to C++11 Standard lambda functionality:

  • Stateless lambdas, which are omni-convertible to function pointers that use arbitrary calling conventions.

  • Automatically deduced return types for lambda bodies that are more complicated than , as long as all return statements have the same type. (This functionality is part of the proposed C++14 Standard.)

C++ Language Reference
Function Objects in the STL
Function Call
for_each

#include <algorithm> #include <cmath> void abssort(float* x, unsigned n) { std::sort(x, x + n, // Lambda expression begins [](float a, float b) { return (std::abs(a) < std::abs(b)); } // end of lambda expression ); }
[&total, factor] [factor, &total] [&, factor] [factor, &] [=, &total] [&total, =]
struct S { void f(int i); }; void S::f(int i) { [&, i]{}; // OK [&, &i]{}; // ERROR: i preceded by & when & is the default [=, this]{}; // ERROR: this when = is the default [i, i]{}; // ERROR: i repeated }
template<class... Args> void f(Args... args) { auto x = [args...] { return g(args...); }; x(); }
pNums = make_unique<vector<int>>(nums); //... auto a = [ptr = move(pNums)]() { // use ptr };
int y = [] (int first, int second) { return first + second; };
auto y = [] (auto first, auto second) { return first + second; };
// throw_lambda_expression.cpp // compile with: /W4 /EHsc int main() // C4297 expected { []() throw() { throw 5; }(); }
auto x1 = [](int i){ return i; }; // OK: return type is int auto x2 = []{ return{ 1, 2 }; }; // ERROR: return type is void, deducing // return type from braced-init-list is not valid
// captures_lambda_expression.cpp // compile with: /W4 /EHsc #include <iostream> usingnamespace std; int main() { int m = 0; int n = 0; [&, n] (int a) mutable { m = ++n + a; }(4); cout << m << endl << n << endl; }
void fillVector(vector<int>& v) { // A local static variable. staticint nextValue = 1; // The lambda expression that appears in the following call to // the generate function modifies and uses the local static // variable nextValue. generate(v.begin(), v.end(), [] { return nextValue++; }); //WARNING: this is not thread-safe and is shown for illustration only }
// compile with: /W4 /EHsc #include <algorithm> #include <iostream> #include <vector> #include <string> usingnamespace std; template <typename C> void print(const string& s, const C& c) { cout << s; for (constauto& e : c) { cout << e << " "; } cout << endl; } void fillVector(vector<int>& v) { // A local static variable. staticint nextValue = 1; // The lambda expression that appears in the following call to // the generate function modifies and uses the local static // variable nextValue. generate(v.begin(), v.end(), [] { return nextValue++; }); //WARNING: this is not thread-safe and is shown for illustration only } int main() { // The number of elements in the vector. constint elementCount = 9; // Create a vector object with each element set to 1. vector<int> v(elementCount, 1); // These variables hold the previous two elements of the vector. int x = 1; int y = 1; // Sets each element in the vector to the sum of the // previous two elements. generate_n(v.begin() + 2, elementCount - 2, [=]() mutablethrow() -> int { // lambda is the 3rd parameter // Generate current value. int n = x + y; // Update previous two values. x = y; y = n; return n; }); print("vector v after call to generate_n() with lambda: ", v); // Print the local variables x and y. // The values of x and y hold their initial values because // they are captured by value. cout << "x: " << x << " y: " << y << endl; // Fill the vector with a sequence of numbers fillVector(v); print("vector v after 1st call to fillVector(): ", v); // Fill the vector with the next sequence of numbers fillVector(v); print("vector v after 2nd call to fillVector(): ", v); }
vector v after call to generate_n() with lambda: 1 1 2 3 5 8 13 21 34 x: 1 y: 1 vector v after 1st call to fillVector(): 1 2 3 4 5 6 7 8 9 vector v after 2nd call to fillVector(): 10 11 12 13 14 15 16 17 18
auto Sqr = [](int t) __declspec(code_seg("PagedMem")) -> int { return t*t; };

Back in December 2015, I discussed the designing of C# 7.0 (msdn.com/magazine/mt595758). A lot has changed over the last year, but the team is now buttoning down C# 7.0 development, and Visual Studio 2017 Release Candidate is implementing virtually all of the new features. (I say virtually because until Visual Studio 2017 actually ships, there’s always a chance for further change.) For a brief overview, you can check out the summary table at itl.tc/CSharp7FeatureSummary. In this article I’m going to explore each of the new features in detail.

Deconstructors

Since C# 1.0, it’s been possible to call a function—the constructor—that combines parameters and encapsulates them into a class. However, there’s never been a convenient way to deconstruct the object back into its constituent parts. For example, imagine a PathInfo class that takes each element of a filename—directory name, file name, extension—and combines them into an object, with support for then manipulating the object’s various elements. Now imagine you wish to extract (deconstruct) the object back into its parts.

In C# 7.0 this becomes trivial via the deconstructor, which returns the specifically identified components of the object. Be careful not to confuse a deconstructor with a destructor (deterministic object deallocation and cleanup) or a finalizer (itl.tc/CSharpFinalizers).

Take a look at the PathInfo class in Figure 1.

Figure 1 PathInfo Class with a Deconstructor with Associated Tests

Obviously, you can call the Deconstruct method as you would have in C# 1.0. However, C# 7.0 provides syntactic sugar that significantly simplifies the invocation. Given the declaration of a deconstructor, you can invoke it using a new C# 7.0 “tuple-like” syntax (see Figure 2).

Figure 2 Deconstructor Invocation and Assignment

Notice how, for the first time, C# is allowing simultaneous assignment to multiple variables of different values. This is not the same as the null assigning declaration in which all variables are initialized to the same value (null):

Instead, with the new tuple-like syntax, each variable is assigned a different value corresponding not to its name, but to the order in which it appears in the declaration and the deconstruct statement.

As you’d expect, the type of the out parameters must match the type of the variables being assigned, and var is allowed because the type can be inferred from Deconstruct parameter types. Notice, however, that while you can put a single var outside the parentheses as shown in Example 3 in Figure 2, at this time it’s not possible to pull out a string, even though all the variables are of the same type.

Note that at this time, the C# 7.0 tuple-like syntax requires that at least two variables appear within the parentheses. For example, (FileInfo path) = pathInfo; is not allowed even if a deconstructor exists for:

In other words, you can’t use the C# 7.0 deconstructor syntax for Deconstruct methods with only one out parameter.

publicclass PathInfo {   publicstring DirectoryName { get; }   publicstring FileName { get; }   publicstring Extension { get; }   publicstring Path   {     get     {       return System.IO.Path.Combine(         DirectoryName, FileName, Extension);     }   }   public PathInfo(string path)   {     DirectoryName = System.IO.Path.GetDirectoryName(path);     FileName = System.IO.Path.GetFileNameWithoutExtension(path);     Extension = System.IO.Path.GetExtension(path);   }   publicvoid Deconstruct(     outstring directoryName, outstring fileName, outstring extension)   {     directoryName = DirectoryName;     fileName = FileName;     extension = Extension;   }   // ... }
PathInfo pathInfo = new PathInfo(@"\\test\unc\path\to\something.ext"); {   // Example 1: Deconstructing declaration and assignment.   (string directoryName, string fileName, string extension) = pathInfo;   VerifyExpectedValue(directoryName, fileName, extension); } {   string directoryName, fileName, extension = null;   // Example 2: Deconstructing assignment.   (directoryName, fileName, extension) = pathInfo;   VerifyExpectedValue(directoryName, fileName, extension); } {   // Example 3: Deconstructing declaration and assignment with var.var (directoryName, fileName, extension) = pathInfo;   VerifyExpectedValue(directoryName, fileName, extension); }
string directoryName, filename, extension = null;
publicvoid Deconstruct(out FileInfo file)

Working with Tuples

As I mentioned, each of the preceding examples leveraged the C# 7.0 tuple-like syntax. The syntax is characterized by the parentheses that surround the multiple variables (or properties) that are assigned. I use the term “tuple-like” because, in fact, none of these deconstructor examples actually leverage any tuple type internally. (In fact, assignment of tuples via a deconstructor syntax isn’t allowed and arguably would be somewhat unnecessary because the object assigned already is an instance representing the encapsulated constituent parts.)

With C# 7.0 there’s now a special streamlined syntax for working with tuples, as shown in Figure 3. This syntax can be used whenever a type specifier is allowed, including declarations, cast operators and type parameters.

Figure 3 Declaring, Instantiating and Using the C# 7.0 Tuple Syntax

For those not familiar with tuples, it’s a way of combining multiple types into a single containing type in a lightweight syntax that’s available outside of the method in which it’s instantiated. It’s lightweight because, unlike defining a class/struct, tuples can be “declared” inline and on the fly. But, unlike dynamic types that also support inline declaration and instantiation, tuples can be accessed outside of their containing member and, in fact, they can be included as part of an API. In spite of the external API support, tuples don’t have any means of version-compatible extension (unless the type parameters themselves happen to support derivation), thus, they should be used with caution in public APIs. Therefore, a preferable approach might be to use a standard class for the return on a public API.

Prior to C# 7.0, the framework already had a tuple class, System.Tuple<…> (introduced in the Microsoft .NET Framework 4). C# 7.0 differs from the earlier solution, however, because it embeds the semantic intent into declaration and it introduces a tuple value type:  System.ValueTuple<…>.

Let’s take a look at the semantic intent. Notice in Figure 3 that the C# 7.0 tuple syntax allows you to declare alias names for each ItemX element the tuple contains. The pathData tuple instance in Figure 3, for example, has strongly typed DirectoryName: string, FileName: string, and Extension: string properties defined, thus allowing calls to pathData.DirectoryName, for example. This is a significant enhancement because prior to C# 7.0, the only names available were ItemX names, where the X incremented for each element.

Now, while the elements for a C# 7.0 tuple are strongly typed, the names themselves aren’t distinguishing in the type definition. Therefore, you can assign two tuples with disparate name aliases and all you’ll get is a warning that informs you the name on the right-hand side will be ignored:

Similarly, you can assign tuples to other tuples that may not have all alias element names defined:

To be clear, the type and order of each element does define type compatibility. Only the element names are ignored. However, even though ignored when they’re different, they still provide IntelliSense within the IDE.

Note that, whether or not an element name alias is defined, all tuples have ItemX names where X corresponds to the number of the element. The ItemX names are important because they make the tuples available from C# 6.0, even though the alias element names are not.

Another important point to be aware of is that the underlying C# 7.0 tuple type is a System.ValueTuple. If no such type is available in the framework version you’re compiling against, you can access it via a NuGet package.

For details about the internals of tuples, check see intellitect.com/csharp7tupleiinternals.

[TestMethod] publicvoid Constructor_CreateTuple() {   (string DirectoryName, string FileName, string Extension) pathData =     (DirectoryName: @"\\test\unc\path\to",     FileName: "something",     Extension: ".ext");   Assert.AreEqual<string>(     @"\\test\unc\path\to", pathData.DirectoryName);   Assert.AreEqual<string>(     "something", pathData.FileName);   Assert.AreEqual<string>(     ".ext", pathData.Extension);   Assert.AreEqual<(string DirectoryName, string FileName, string Extension)>(     (DirectoryName: @"\\test\unc\path\to",       FileName: "something", Extension: ".ext"),     (pathData));   Assert.AreEqual<(string DirectoryName, string FileName, string Extension)>(     (@"\\test\unc\path\to", "something", ".ext"),     (pathData));   Assert.AreEqual<(string, string, string)>(     (@"\\test\unc\path\to", "something", ".ext"), (pathData));   Assert.AreEqual<Type>(     typeof(ValueTuple<string, string, string>), pathData.GetType()); } [TestMethod] publicvoid ValueTuple_GivenNamedTuple_ItemXHasSameValuesAsNames() {   var normalizedPath =     (DirectoryName: @"\\test\unc\path\to", FileName: "something",     Extension: ".ext");   Assert.AreEqual<string>(normalizedPath.Item1, normalizedPath.DirectoryName);   Assert.AreEqual<string>(normalizedPath.Item2, normalizedPath.FileName);   Assert.AreEqual<string>(normalizedPath.Item3, normalizedPath.Extension); } staticpublic (string DirectoryName, string FileName, string Extension)   SplitPath(string path) {   // See http://bit.ly/2dmJIMm Normalize method for full implementation.return (               System.IO.Path.GetDirectoryName(path),     System.IO.Path.GetFileNameWithoutExtension(path),     System.IO.Path.GetExtension(path)     ); }
// Warning: The tuple element name 'AltDirectoryName1' is ignored// because a different name is specified by the target type... (string DirectoryName, string FileName, string Extension) pathData =   (AltDirectoryName1: @"\\test\unc\path\to",   FileName: "something", Extension: ".ext");
// Warning: The tuple element name 'directoryName', 'FileNAme' and 'Extension'// are ignored because a different name is specified by the target type... (string, string, string) pathData =   (DirectoryName: @"\\test\unc\path\to", FileName: "something", Extension: ".ext");

Pattern Matching with Is Expressions

On occasion you have a base class, Storage for example, and a series of derived classes, DVD, UsbKey, HardDrive, FloppyDrive (remember those?) and so on. To implement an Eject method for each you have several options:

  • As Operator
    • Cast and assign using the as operator
    • Check the result for null
    • Perform the eject operation
  • Is Operator
    • Check the type using the is operator
    • Cast and assign the type
    • Perform the eject operation
  • Cast
    • Explicit cast and assign
    • Catch possible exception
    • Perform operation
    • Yuck!

There’s a fourth, far-better approach using polymorphism in which you dispatch using virtual functions. However, this is available only if you have the source code for the Storage class and can add the Eject method. That’s an option I’m assuming is unavailable for this discussion, hence the need for pattern matching.

The problem with each of these approaches is that the syntax is fairly verbose and always requires multiple statements for each class to which you want to cast. C# 7.0 provides pattern matching as a means of combining the test and the assignment into a single operation. As a result, the code in Figure 4 simplifies down to what’s shown in Figure 5.

Figure 4 Type Casting Without Pattern Matching

Figure 5 Type Casting with Pattern Matching

The difference between the two isn’t anything radical, but when performed frequently (for each of the derived types, for example) the former syntax is a burdensome C# idiosyncrasy. The C# 7.0 improvement—combining the type test, declaration and assignment into a single operation—renders the earlier syntax all but depre­cated. In the former syntax, checking the type without assigning an identifier makes falling through to the “default” else cumbersome at best. In contrast, the assignment allows for the additional conditionals beyond just the type check.

Note that the code in Figure 5 starts out with a pattern-matching is operator with support for a null comparison operator, as well:

// Eject without pattern matching.publicvoid Eject(Storage storage) {   if (storage == null)   {     thrownew ArgumentNullException();   }   if (storage is UsbKey)   {     UsbKey usbKey = (UsbKey)storage;     if (usbKey.IsPluggedIn)     {       usbKey.Unload();       Console.WriteLine("USB Drive Unloaded.");     }     elsethrownew NotImplementedException();    }   elseif(storage is DVD)   // ...elsethrownew NotImplementedException(); }
// Eject with pattern matching.publicvoid Eject(Storage storage) {   if (storage isnull)   {     thrownew ArgumentNullException();   }   if ((storage is UsbKey usbDrive) && usbDrive.IsPluggedIn)   {     usbDrive.Unload();     Console.WriteLine("USB Drive Unloaded.");   }   elseif (storage is DVD dvd && dvd.IsInserted)   // ...elsethrownew NotImplementedException();  // Default }
if (storage isnull) { ... }

Pattern Matching with the Switch Statement

While supporting pattern matching with the is operator provides an improvement, pattern-matching support for a switch statement is arguably even more significant, at least when there are multiple compatible types to which to convert. This is because C# 7.0 includes case statements with pattern matching and, furthermore, if the type pattern is satisfied in the case statement, an identifier can be provided, assigned, and accessed all within the case statement. Figure 6 provides an example.

Figure 6 Pattern Matching in a Switch Statement

Notice in the example how local variables like usbKey and dvd are declared and assigned automatically within the case statement. And, as you’d expect, the scope is limited to within the case statement.

Perhaps just as important as the variable declaration and assignment, however, is the additional conditional that can be appended to the case statement with a when clause. The result is that a case statement can completely filter out an invalid scenario without an additional filter inside the case statement. This has the added advantage of allowing evaluation of the next case statement if, in fact, the former case statement is not fully met. It also means that case statements are no longer limited to constants and, furthermore, a switch expression can be any type—it’s no longer limited to bool, char, string, integral and enum.

Another important characteristic the new C# 7.0 pattern-matching switch statement capability introduces is that case statement order is significant and validated at compile time. (This is in contrast with earlier versions of the language, in which, without pattern matching, case statement order was not significant.) For example, if I introduced a case statement for Storage prior to a pattern-matching case statement that derives from Storage (UsbKey, DVD and HardDrive), then the case Storage would eclipse all other type pattern matching (that derives from Storage). A case statement from a base type that eclipses other derived type case statements from evaluation will result in a compile error on the eclipsed case statement. In this way, case statement order requirements are similar to catch statements.

Readers will recall that an is operator on a null value will return false. Therefore, no type pattern-matching case statement will match for a null-valued switch expression. For this reason, order of the null case statement won’t matter; this behavior matches switch statements prior to pattern matching.

Also, in support of compatibility with switch statements prior to C# 7.0, the default case is always evaluated last regardless of where it appears in the case statement order. (That said, readability would generally favor putting it at the end, because it’s always evaluated last.) Also, goto case statements still work only for constant case labels—not for pattern matching.

Local Functions

While it’s already possible to declare a delegate and assign it an expression, C# 7.0 takes this one step further by allowing the full declaration of a local function inline within another member. Consider the IsPalindrome function in Figure 7.

Figure 7 A Local Function Example

In this implementation, I first check that the argument passed to IsPalindrome isn’t null or only whitespace. (I could’ve used pattern matching with “text is null” for the null check.) Next, I declare a function LocalIsPalindrome in which I compare the first and last characters recursively. The advantage of this approach is that I don’t declare the LocalIsPalindrome within the scope of the class where it can potentially be called mistakenly, thus circumventing the IsNullOrWhiteSpace check. In other words, local functions provide an additional scope restriction, but only inside the surrounding function.

The parameter validation scenario in Figure 7 is one of the common local function use cases. Another one I encounter frequently occurs within unit tests, such as when testing the IsPalindrome function (see Figure 8).

Figure 8 Unit Testing Often Uses Local Functions

Iterator functions that return IEnumerable<T> and yield return elements are another common local function use case.

To wrap up the topic, here are a few more points to be aware of for local functions:

  • Local functions don’t allow use of an accessibility modifier (public, private, protected).
  • Local functions don’t support overloading. You can’t have two local functions in the same method with the same name even if the signatures don’t overlap.
  • The compiler will issue a warning for local functions that are never invoked.
  • Local functions can access all variables in the enclosing scope, including local variables. This behavior is the same with locally defined lambda expressions except that local functions don’t allocate an object that represents the closure, as locally defined lambda expressions do.
  • Local functions are in scope for the entire method, regardless of whether they’re invoked before or after their declaration.
publicvoid Eject(Storage storage) {   switch(storage)   {     case UsbKey usbKey when usbKey.IsPluggedIn:       usbKey.Unload();       Console.WriteLine("USB Drive Unloaded.");       break;     case DVD dvd when dvd.IsInserted:       dvd.Eject();       break;     case HardDrive hardDrive:       thrownew InvalidOperationException();     casenull:     default:       thrownew ArgumentNullException();   } }
bool IsPalindrome(string text) {   if (string.IsNullOrWhiteSpace(text)) returnfalse;   bool LocalIsPalindrome(string target)   {     target = target.Trim();  // Start by removing any surrounding whitespace.if (target.Length <= 1) returntrue;     else     {       returnchar.ToLower(target[0]) ==         char.ToLower(target[target.Length - 1]) &&         LocalIsPalindrome(           target.Substring(1, target.Length - 2));     }   }   return LocalIsPalindrome(text); }
[TestMethod] publicvoid IsPalindrome_GivenPalindrome_ReturnsTrue() {   void AssertIsPalindrome(string text)   {     Assert.IsTrue(IsPalindrome(text),       $"'{text}' was not a Palindrome.");   }   AssertIsPalindrome("7");   AssertIsPalindrome("4X4");   AssertIsPalindrome("   tnt");   AssertIsPalindrome("Was it a car or a cat I saw");   AssertIsPalindrome("Never odd or even"); }

Return by Reference

Since C# 1.0 it has been possible to pass arguments into a function by reference (ref). The result is that any change to the parameter itself will get passed back to the caller. Consider the following Swap function:

In this scenario, the called method can update the original caller’s variables with new values, thereby swapping what’s stored in the first and second arguments.

Starting in C# 7.0, you’re also able to pass back a reference via the function return—not just a ref parameter. Consider, for example, a function that returns the first pixel in an image that’s associated with red-eye, as shown in Figure 9.

Figure 9 Ref Return and Ref Local Declaration

By returning a reference to the image, the caller is then able to update the pixel to a different color. Checking for the update via the array shows that the value is now black. The alternative of using a by reference parameter is, one might argue, less obvious and less readable:

There are two important restrictions on return by reference—both due to object lifetime. These are that object references shouldn’t be garbage collected while they’re still referenced, and they shouldn’t consume memory when they no longer have any references. First, you can only return references to fields, other reference-returning properties or functions, or objects that were passed in as parameters to the by reference-returning function. For example, FindFirst­RedEyePixel returns a reference to an item in the image array, which was a parameter to the function. Similarly, if the image was stored as a field within a class, you could return the field by reference:

Second, ref locals are initialized to a certain storage location in memory, and can’t be modified to point to a different location. (You can’t have a pointer to a reference and modify the reference—a pointer to a pointer for those of you with a C++ background.)

There are several return-by-reference characteristics of which to be cognizant: 

  • If you’re returning a reference you obviously have to return it. This means, therefore, that in the example in Figure 9, even if no red-eye pixel exists, you still need to return a ref byte. The only workaround would be to throw an exception. In contrast, the by reference parameter approach allows you to leave the parameter unchanged and return a bool indicating success. In many cases, this might be preferable.
  • When declaring a reference local variable, initialization is required. This involves assigning it a ref return from a function or a reference to a variable:
  • Although it’s possible in C# 7.0 to declare a reference local variable, declaring a field of type ref isn’t allowed:
  • You can’t declare a by reference type for an auto-implemented property:
staticvoid Swap(refstring x, refstring y)
publicrefbyte FindFirstRedEyePixel(byte[] image) {   //// Do fancy image detection perhaps with machine learning.for (int counter = 0; counter < image.Length; counter++)   {     if(image[counter] == (byte)ConsoleColor.Red)     {       returnref image[counter];     }   }   thrownew InvalidOperationException("No pixels are red."); } [TestMethod] publicvoid FindFirstRedEyePixel_GivenRedPixels_ReturnFirst() {   byte[] image;   // Load image.// ...// Obtain a reference to the first red pixel.refbyte redPixel = ref FindFirstRedEyePixel(image);   // Update it to be Black.   redPixel = (byte)ConsoleColor.Black;   Assert.AreEqual<byte>((byte)ConsoleColor.Black, image[redItems[0]]); }
publicbool FindFirstRedEyePixel(refbyte pixel);
byte[] _Image; publicrefbyte[] Image { get {  returnref _Image; } }
refstring text;  // Error
class Thing { refstring _Text;  /* Error */ }

0 thoughts on “Referenced Before Assignment In Enclosing Scope Scholastic

Leave a Reply

Your email address will not be published. Required fields are marked *