Understanding Scope and Visibility in C#
Mar 18, 2020 • 8 Minute Read
Introduction
In this guide, we will talk about two crucial concepts in C#: visibility and scope. Most programming languages out there incorporate these two concepts. They lay out the structure in which your data lives and show the direction they are allowed to flow. First we'll take a deeper dive into scope, then we'll turn our sights to visibility.
Scope
This concept defines the part of the application where the variable is accessible, called the scope of a variable. Variables can be defined in classes, methods, loops, and structs.
There are three main scopes for a variable:
- Class level
- Method level
- Block level (Nested scope)
Class Level
Let's create some code to demonstrate this.
using System;
namespace Pluralsight
{
public class ClassLevel
{
int a = 6;
string b = "This is a string";
void ex() {
double c = 10.0;
Console.WriteLine($"The value c is {c}");
}
public static void Main()
{
ClassLevel a = new ClassLevel();
Console.WriteLine($"The value a is {a.a}");
Console.WriteLine($"The value b is {a.b}");
ClassLevel.ex();
Console.ReadKey();
}
}
}
The output is the following.
The value a is 6
The value b is This is a string
The value c is 10
When we declare a variable in a class outside any method, it can be accessed anywhere in the class, just like in our case above with the two variables a and b. When you define variables this way, they are called fields or class members. Class level scoped variables, like c in our case, can only be accessed by the non-static method of the class in which they were defined. If we define an access modifier to a class level variable, it does not effect its scope within the class. Access modifiers provide access to fields from outside the class.
Method Level
Let's create an example to demonstrate this.
using System;
namespace Pluralsight
{
public class MethodLevel
{
double c;
void first() {
double c = 10.0;
Console.WriteLine($"The value c is {c}");
}
void second() {
Console.WriteLine($"The value c is {c}");
}
public static void Main()
{
MethodLevel a = new MethodLevel();
Console.WriteLine($"The value c is {a.c}");
a.first();
a.second();
Console.ReadKey();
}
}
}
The output is as follows.
The value c is 0
The value c is 10
The value c is 0
This demonstrates how methods encapsulate their own namespace and the variables defined within. We can see how the prints for the value of c are different in this case. Whenever we define a variable within a method, it is assigned the method level scope. and we cannot access it outside this method. This variable cannot overwrite or be overwritten by a variable of the same name from a higher scope. These variables are called local variables. We cannot declare them twice with the same name in the same scope. After the method is executed, the variables cease to exist, as they are collected by the ever-greedy garbage collector.
Block Level
Let's also demonstrate this with an example.
using System;
namespace Pluralsight
{
public class BlockLevel
{
public static void Main()
{
double roentgen = 3.6;
if (roentgen >=3.6)
{ string message = "Not great, not terrible!"; }
else
{ string message = "Well, could be worse!"; }
Console.WriteLine($"Status report: {message");
Console.ReadKey();
}
}
}
The above code will not compile or execute. The problem is indicated by the compiler in two stages. The first message we get is that the value of the message variable was declared but never used, and the second message we get is that the message variable does not exist in the current context.
We can circumvent this by modifying the message not to be a string declared in the if, but either bring it up to the scope of the Main() function or make it a class field.
These variables are typically useful when declared inside a for or while loop. Sometimes they are referred to as loop variables because of their limited scope. When you declare a variable outside a loop, it's still accessible within nested loops, which means a class level variable will be accessible to the methods and the loops as well. The method level will be accessible to the method and the loop. The variable declared inside a loop body will not be accessible from higher scopes.
Visibility
When we talk about visibility, we need to consider class,method, and variable constructs. Visibility tells us how these items can be accessed.
There are five types of visibility:
- Public
- Protected
- Internal
- Protected internal
- Private
Public
This is the most forgiving visibility type. The member defined this way can be reached from anywhere, by default enums and interfaces are public.
Protected
Members specified this way can only be reached from the same class, or from within the class.
Internal
Members specified this way can only be reached from the same project.
Protected Internal
Members specified this way can only be reached from the same project, and those classes that inherit from the class, even from another project.
Private
Members specified this way can only be reached by other members of the same class. By default, classes and structs are set to this visibility level, and this is the most restrictive.
Let's create a small demonstration code.
using System;
namespace Pluralsight
{
public class AnotherVisibility {
public string message = "I am visible from anywhere!";
protected string pmessage = "Only from the same class!";
internal string imessage = "I am visible only from within the same project!";
private string ppmessage = "Untouchable!";
public void ProtectedMessage() {
Console.WriteLine(pmessage);
Console.WriteLine(ppmessage);
}
}
public class Visibility : AnotherVisibility
{
public static void Main()
{
Visibility a = new Visibility();
AnotherVisibility b = new AnotherVisibility();
Console.WriteLine(a.message);
Console.WriteLine(a.imessage);
Console.WriteLine(a.pmessage);
b.ProtectedMessage();
Console.ReadKey();
}
}
}
The output is as follows.
I am visible from anywhere!
I am visible only from within the same project!
Only from the same class!
Only from the same class!
Untouchable!
Here we can see the concepts in action. Our message variable, since it is public, is accessible from anywhere. We have our pmessage variable, which is accessible from the same or inherited classes. We have the imessage variable, which is accessible from the same or inherited classes. We also have the ppmessage variable, which is only accessible from the original class.
Conclusion
In this guide, we clarified the concepts of scope and visibility. Using live examples, we saw how to write more efficient code that is easier to maintain. At first, learning to use these concepts takes some time, but later they will become second nature. I hope this guide has been informative to you and I would like to thank you for reading it!