Polyglot
Languages C# scope
C# § scope

Namespaces

C# uses namespaces — named regions in which types and members live — and several scopes within them: the namespace scope, the class scope, the method scope, and the block scope. The namespace mechanism is the conventional way to organise large codebases; the standard library is itself organised into a hierarchy of namespaces under System. Beyond namespaces, the language admits several visibility modifiers — public, private, protected, internal, protected internal, private protected, and (since C# 11) file — that control which code may access a member.

C# does not have C++‘s argument-dependent lookup or its anonymous-namespace mechanism. The conventions are correspondingly cleaner: name resolution proceeds outward from the use site through the enclosing scopes, with using directives bringing names into scope from elsewhere.

The kinds of scope

C# distinguishes several scopes:

  • Block scope — the curly-brace-delimited region in which a declaration appears. Method bodies, compound statements, and for-loop init clauses introduce block scope.
  • Method scope — the entirety of a method body. Method parameters and locals declared in the method body have method or block scope.
  • Type scope — the scope of members within a class, struct, record, or interface. Members are accessible by their unqualified name within other members.
  • Namespace scope — the scope of types declared at the top level of a namespace.
namespace MyApp {
    class Counter {
        private int count = 0;          // type-scope (field)

        public void Increment() {
            int delta = 1;              // method-scope local

            if (delta > 0) {
                int times = 1;          // block-scope
                count += delta * times;
            }
        }
    }
}

A name in an inner scope shadows a name in an enclosing scope. C# admits shadowing for locals, parameters, and members in narrow circumstances; the compiler diagnoses the most common ambiguities.

Namespaces

A namespace is a named region containing types. The conventional structure of a C# project mirrors the namespace hierarchy:

namespace Company.Product {
    public class Widget {
        // ...
    }
}

namespace Company.Product.Internal {
    public class Helper {
        // ...
    }
}

The dot-separated form Company.Product declares a nested namespace. Equivalent nested forms:

namespace Company {
    namespace Product {
        public class Widget { /* ... */ }
    }
}

namespace Company.Product {
    public class Widget { /* ... */ }
}

The flat form (Company.Product) is the conventional choice.

File-scoped namespaces

C# 10 introduced file-scoped namespaces: a single namespace for the entire file, with no explicit braces:

namespace Company.Product;       // applies to the whole file

public class Widget {
    // ...
}

public class Gadget {
    // ...
}

The form reduces indentation and is the conventional choice in new code targeting C# 10 or later. Each file may have only one file-scoped namespace declaration, and it must precede all type declarations.

The using directive

The using directive brings types from another namespace into scope:

using System;
using System.Collections.Generic;
using System.Linq;

class Program {
    static void Main() {
        var list = new List<int> { 1, 2, 3, 4, 5 };
        var sum  = list.Sum();
        Console.WriteLine(sum);
    }
}

Without the directive, types must be referred to by their fully-qualified name (System.Console.WriteLine, System.Collections.Generic.List<int>).

using static

The using static directive brings static members of a type into scope, so that they may be called without the type-name qualification:

using static System.Math;

double area = PI * radius * radius;     // PI from System.Math
double root = Sqrt(area);               // Sqrt from System.Math

The construction is convenient for math-heavy code and for code that uses utility classes whose name adds no information.

Type aliases

A using directive admits an alias:

using StringList = System.Collections.Generic.List<string>;
using Json       = System.Text.Json.JsonSerializer;

StringList names = new StringList();

Aliases are useful when a type has a name that conflicts with a name in another namespace, or when a long generic type name needs a short alias.

C# 12 admitted aliases for almost any type, including tuple types, pointer types, array types, and unsafe types.

Global usings

C# 10 introduced global using directives — using statements that apply to every file in the project:

// in any .cs file (conventionally GlobalUsings.cs):
global using System;
global using System.Collections.Generic;
global using System.Linq;

The directive applies to the entire compilation. The mechanism is the conventional way to avoid repeating common using directives in every file. Project files may also enable implicit usings, which adds a curated set of common namespaces:

<PropertyGroup>
    <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

With implicit usings enabled (the default in newer SDK templates), System, System.Collections.Generic, System.Linq, System.Threading.Tasks, and several others are automatically available in every file.

Access modifiers

Seven access levels:

ModifierVisibility
publicAny code, in any assembly.
internalAny code in the same assembly.
protectedThe declaring type and its derived types.
privateThe declaring type only.
protected internalSame assembly OR derived types.
private protectedSame assembly AND derived types (intersection).
fileThe declaring file only (C# 11).

The default access for top-level types is internal; for members of a class or struct it is private. The conventional discipline:

  • public for the API: the surface intended for callers.
  • internal for assembly-internal helpers.
  • private for type-internal implementation details.
  • protected rarely; only when the type is explicitly designed for inheritance.
  • file for one-off types in a generated source file.
public class Widget {
    public  string  Name      { get; init; } = "";    // visible everywhere
    internal int    Revision  { get; set; } = 0;       // visible in the assembly
    private  int    refCount  = 0;                      // visible to Widget only

    public void Use() { ++refCount; }
}

internal and the assembly

The assembly is the unit of internal visibility. An assembly is a .dll or .exe produced by compiling one or more source files together; it has its own metadata and version. internal members are visible to all code in the same assembly, regardless of which file or namespace the code is in.

The [InternalsVisibleTo("OtherAssembly")] attribute extends internal access to a specific other assembly. The conventional use is for unit-test projects, which need access to internal members of the project under test:

[assembly: InternalsVisibleTo("MyProject.Tests")]

File-scoped types

C# 11 admitted the file access modifier:

// in MyComponent.cs
file class Helper {
    /* visible only within MyComponent.cs */
}

public class MyComponent {
    private Helper helper = new Helper();
}

file access is principally useful for source generators that produce types whose names should not collide with user code; the file-scoping prevents the collision.

Nested types

A type may be declared inside another type:

public class Container {
    public class Iterator {
        // nested
    }

    private class Internal {
        // nested, private
    }
}

Container.Iterator it = new Container.Iterator();

Nested types live in the enclosing type’s scope; their access is governed by the type’s own modifier (so a public class Internal inside a public class Container is reachable as Container.Internal from anywhere).

The conventions:

  • Nested types are appropriate when the inner type is tightly coupled to the outer (an iterator over a custom collection, a state enum specific to one type).
  • A nested type with internal or private access is a way to express a helper that is invisible elsewhere.

The default namespace and the global namespace

A C# file with no namespace declaration places its top-level types in the global namespace:

// NoNamespace.cs
public class TopLevel { /* ... */ }    // in the global namespace

The global:: qualifier explicitly references the global namespace:

global::System.Console.WriteLine("hello");

The qualifier is rarely needed; it disambiguates in cases where a local namespace shadows a name from the global namespace.

Common patterns

Selective using for clarity

using System;
using System.Collections.Generic;
using System.Linq;

Most files have a small block of using directives at the top, for the namespaces actually used in the file.

using static for math and constants

using static System.Math;
using static System.Console;

WriteLine($"area: {PI * r * r}");

The construction reads more like the source it imitates and is conventional for math-heavy or output-heavy code.

Aliases for disambiguation

using HttpClient = System.Net.Http.HttpClient;
using OrderClient = MyApp.Internal.HttpClient;     // resolves a name conflict

When two namespaces define types with the same name, an alias picks one and disambiguates the other.

Internal helpers in a separate namespace

namespace MyApp.Internal {
    internal class Helper { /* ... */ }
}

namespace MyApp {
    public class Public {
        public void Method() {
            var h = new Internal.Helper();   // accessible: same assembly
        }
    }
}

The convention separates internal types from public types at the namespace level. The access modifier (internal) is what enforces the boundary; the namespace is documentation.