C# Escopo
Toda variável tem um "território": o trecho do código em que o nome dela existe. Esse território se chama escopo. Fora dele, o compilador simplesmente não conhece o nome, e você recebe o famoso O nome "x" não existe no contexto atual. Nesta aula você vai aprender a regra das chaves, ver como ela vale para if, for e métodos, e entender um ponto que confunde muita gente no C# moderno: os métodos do fim de um arquivo com instruções de nível superior enxergam as variáveis do programa, mas os métodos de uma classe, não.
- A regra das chaves: uma variável existe do ponto em que é declarada até o
}do bloco onde nasceu. - O escopo das variáveis criadas dentro de
if,foreforeach. - Blocos irmãos (o mesmo nome pode) e blocos aninhados (o mesmo nome não pode).
- O escopo das variáveis e parâmetros de um método.
- Por que as funções locais enxergam as variáveis do programa, e como o
staticimpede isso. - O tempo de vida: por que uma variável de método recomeça do zero a cada chamada.
- Os erros
CS0103,CS0136,CS0165,CS0841eCS8421.
Todos os programas desta aula rodaram na máquina fictícia da Ana, na pasta ~/cs, com o .NET 10 e o comando dotnet run arquivo.cs.
A ideia: os cômodos da casa
Pense na casa da Ana. O que está na sala pode ser visto de qualquer lugar da sala, e também de dentro do quarto, se a porta estiver aberta para ela. Mas o que está guardado dentro do quarto não aparece na sala: quem está na sala não vê. E, quando a Ana arruma o quarto e fecha a porta, o que estava lá some de vista.
Em C#, os cômodos são os blocos, os trechos entre { e }. Um bloco dentro de outro enxerga o de fora; o de fora não enxerga o de dentro.
| Na casa | Em C# |
|---|---|
| Um cômodo | Um bloco { … }: o corpo de um if, de um laço, de um método |
| Um objeto deixado no cômodo | Uma variável declarada no bloco |
| Do quarto dá para ver a sala | Um bloco de dentro enxerga as variáveis dos blocos de fora |
| Da sala não dá para ver dentro do quarto | Fora do bloco, a variável não existe (CS0103) |
1. A regra das chaves
Uma variável existe a partir da linha em que é declarada até o } que fecha o bloco em que ela nasceu. Este programa tem três níveis de blocos (chaves soltas são permitidas, só para mostrar a ideia):
int nivel1 = 1;
{
int nivel2 = 2;
{
int nivel3 = 3;
Console.WriteLine($"{nivel1} {nivel2} {nivel3}");
}
Console.WriteLine($"{nivel1} {nivel2}");
}
Console.WriteLine($"{nivel1}");
$ dotnet run escada.cs 1 2 3 1 2 1
- No bloco mais de dentro, as três variáveis existem: o bloco enxerga os de fora.
- Depois do primeiro
},nivel3deixou de existir. Sobraramnivel1enivel2. - Depois do segundo
}, sónivel1, que nasceu fora de qualquer bloco, no nível do programa.
E "a partir da linha em que é declarada" quer dizer que usar antes também não vale:
Console.WriteLine(preco); decimal preco = 9.90m;
$ dotnet run antesdecl.cs /home/ana/cs/antesdecl.cs(1,19): error CS0841: Não é possível usar a variável local "preco" antes de declará-la Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
2. Variáveis de if e de laços
O corpo de um if é um bloco. O que nasce lá dentro morre no } dele:
int idade = 20;
if (idade >= 18)
{
string mensagem = "Pode dirigir";
Console.WriteLine(mensagem);
}
Console.WriteLine(mensagem);
$ dotnet run bloco.cs /home/ana/cs/bloco.cs(7,19): error CS0103: O nome "mensagem" não existe no contexto atual Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O nome "mensagem" não existe no contexto atual (The name 'mensagem' does not exist in the current context). "Contexto atual" é o escopo da linha 7, que fica fora do if. A variável nasceu na linha 4 e morreu no } da linha 6. É o mesmo erro de um nome digitado errado, mas a causa é outra: o nome está certo, só que no lugar errado.
O conserto é declarar a variável antes do if, no escopo em que ela vai ser usada, e só dar o valor dentro:
int idade = 20;
string mensagem;
if (idade >= 18)
{
mensagem = "Pode dirigir";
}
else
{
mensagem = "Ainda não";
}
Console.WriteLine(mensagem);
$ dotnet run blocook.cs Pode dirigir
Repare no else. Sem ele, se a idade fosse 15, mensagem chegaria na última linha sem valor, e o compilador recusaria com CS0165: Uso de variável local não atribuída "mensagem". Ele confere todos os caminhos, como faz com o return (Valor de Retorno).
Missão: o programa não compila (CS0103): a última linha usa mensagem fora do bloco em que ela nasceu. Conserte para que o programa mostre Pode dirigir uma vez só, pela última linha (tire o WriteLine de dentro do if).
int idade = 20;
if (idade >= 18)
{
string mensagem = "Pode dirigir";
Console.WriteLine(mensagem);
}
Console.WriteLine(mensagem);
int idade = 20;
string mensagem = "Ainda não";
if (idade >= 18)
{
mensagem = "Pode dirigir";
}
Console.WriteLine(mensagem);
A variável do for
A variável criada nos parênteses do for pertence ao laço. Quando ele termina, ela some:
for (int i = 1; i <= 3; i++)
{
Console.WriteLine($"Volta {i}");
}
Console.WriteLine($"Terminou com i = {i}");
$ dotnet run forvar.cs /home/ana/cs/forvar.cs(5,39): error CS0103: O nome "i" não existe no contexto atual Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O mesmo vale para a variável do foreach (foreach (string item in lista)). Se você precisa do valor depois do laço, declare a variável antes dele.
Blocos irmãos e blocos aninhados
Como cada laço tem a sua variável, dois laços um depois do outro podem usar o mesmo nome. São blocos irmãos: quando o segundo começa, a variável do primeiro já morreu.
for (int i = 1; i <= 2; i++)
{
Console.Write($"a{i} ");
}
for (int i = 1; i <= 2; i++)
{
Console.Write($"b{i} ");
}
Console.WriteLine();
if (true)
{
int x = 1;
Console.WriteLine($"primeiro x = {x}");
}
if (true)
{
int x = 2;
Console.WriteLine($"segundo x = {x}");
}
$ dotnet run irmaos.cs a1 a2 b1 b2 primeiro x = 1 segundo x = 2
Já um bloco dentro de outro não pode criar uma variável com o nome de uma que ainda está viva lá fora. Do bloco de dentro, as duas seriam visíveis ao mesmo tempo, e o nome ficaria ambíguo:
int total = 10;
if (total > 5)
{
int total = 20;
Console.WriteLine(total);
}
$ dotnet run aninhado.cs /home/ana/cs/aninhado.cs(4,9): error CS0136: Um local ou um parâmetro denominado "total" não pode ser declarado neste escopo porque esse nome é usado em um escopo delimitador de local para definir um local ou parâmetro Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
"Escopo delimitador" é o bloco de fora, o que "abraça" o if. Em inglês: A local or parameter named 'total' cannot be declared in this scope because that name is used in an enclosing local scope. Se a intenção era mudar o valor, tire o tipo (total = 20;). Se era outra variável, dê outro nome.
3. O escopo de um método
O corpo de um método também é um bloco. As variáveis declaradas lá dentro, e os parâmetros, só existem dentro dele:
CalcularTotal();
Console.WriteLine(total);
void CalcularTotal()
{
decimal total = 12.50m * 3;
Console.WriteLine($"Dentro: {total}");
}
$ dotnet run metodolocal.cs /home/ana/cs/metodolocal.cs(2,19): error CS0103: O nome "total" não existe no contexto atual Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O total mora dentro de CalcularTotal. Para levar um valor de dentro do método para fora, o caminho certo é o return: decimal total = CalcularTotal();.
Cada chamada começa do zero
Além do onde (o escopo), toda variável tem um quando: o tempo de vida. Uma variável declarada dentro de um método nasce quando a chamada começa e morre quando ela termina. A chamada seguinte cria uma variável nova:
Contar();
Contar();
Contar();
void Contar()
{
int vezes = 0;
vezes++;
Console.WriteLine($"vezes = {vezes}");
}
$ dotnet run reinicia.cs vezes = 1 vezes = 1 vezes = 1
Nunca passa de 1: em cada chamada, vezes nasce com 0, vira 1, é mostrada e morre. Para um contador que dure entre as chamadas, ele tem de morar fora do método. É a próxima seção.
4. Funções locais enxergam as variáveis do programa
Em Métodos você viu que, num arquivo com instruções de nível superior, os métodos do fim do arquivo são funções locais do Main escondido. Elas moram dentro do mesmo bloco que as variáveis do programa. Pela regra das chaves, então, elas enxergam essas variáveis:
string loja = "Lojinha da Ana";
MostrarCabecalho();
void MostrarCabecalho()
{
Console.WriteLine($"=== {loja} ===");
}
$ dotnet run captura1.cs === Lojinha da Ana ===
O método usou loja sem receber nada por parâmetro. Isso se chama capturar a variável. E a captura não é uma cópia: a função local mexe na própria variável. É assim que se faz um contador que dura entre as chamadas:
int vendas = 0;
Vender();
Vender();
Vender();
Console.WriteLine($"Vendas do dia: {vendas}");
void Vender()
{
vendas++;
}
$ dotnet run captura2.cs Vendas do dia: 3
Na forma com classe, não funciona
Agora o mesmo cabeçalho na forma com classe. Aqui, MostrarCabecalho é um método da classe, irmão do Main, e não está dentro dele:
class Program
{
static void Main()
{
string loja = "Lojinha da Ana";
MostrarCabecalho();
}
static void MostrarCabecalho()
{
Console.WriteLine(loja);
}
}
$ dotnet run classeescopo.cs /home/ana/cs/classeescopo.cs(5,16): warning CS0219: A variável "loja" é atribuída, mas seu valor nunca é usado /home/ana/cs/classeescopo.cs(11,27): error CS0103: O nome "loja" não existe no contexto atual Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Pela regra das chaves, loja mora no bloco do Main; o bloco de MostrarCabecalho fica do lado de fora. Para o compilador, o loja do Main nunca é usado (o aviso CS0219), e o da linha 11 não existe. Na forma com classe, os dados passam de um método a outro por parâmetros (MostrarCabecalho(loja)) ou ficam num campo da classe:
class Program
{
static int vendas = 0;
static void Main()
{
Vender();
Vender();
Console.WriteLine($"Vendas: {vendas}");
}
static void Vender()
{
vendas++;
}
}
$ dotnet run campo.cs Vendas: 2
O static int vendas, declarado direto na classe (fora de qualquer método), é um campo: o escopo dele é a classe inteira, e ele vive enquanto o programa roda. Campos são o assunto de Campos e static.
| Função local (fim do arquivo) | Método de classe (static ao lado do Main) | |
|---|---|---|
| Onde mora | Dentro do Main escondido | Na classe, ao lado do Main |
| Enxerga as variáveis do programa? | Sim (captura) | Não (CS0103) |
| Como recebe dados | Parâmetros ou captura | Parâmetros ou campos da classe |
Os cuidados com a captura
Capturar é prático, mas tem dois perigos.
1. A variável precisa ter valor quando a função roda. Uma função local pode ser chamada antes de ser escrita, mas a variável que ela usa, não:
MostrarCabecalho();
string loja = "Lojinha da Ana";
void MostrarCabecalho()
{
Console.WriteLine($"=== {loja} ===");
}
$ dotnet run capturaordem.cs /home/ana/cs/capturaordem.cs(1,1): error CS0165: Uso de variável local não atribuída "loja" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Repare onde o erro aponta: a linha 1, a chamada. Naquele momento, loja ainda não tem valor, e o método ia lê-la.
2. Fica escondido o que o método usa e muda. Quem lê Vender(); não sabe que vendas mudou; para descobrir, tem de abrir o método. Num programa pequeno, tudo bem. Num grande, um método que mexe em variáveis "de longe" é uma fonte clássica de bugs.
static: proibir a captura
Se você escrever static antes de uma função local C# 8, o compilador proíbe que ela use as variáveis de fora. Tudo o que ela precisar tem de chegar por parâmetro:
string loja = "Lojinha da Ana";
MostrarCabecalho();
static void MostrarCabecalho()
{
Console.WriteLine($"=== {loja} ===");
}
$ dotnet run estaticolocal.cs /home/ana/cs/estaticolocal.cs(6,30): error CS8421: Uma função local estática não pode conter uma referência a 'loja'. Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Conserto: static void MostrarCabecalho(string loja) e a chamada MostrarCabecalho(loja);. Com o static, a função local se comporta como um método de classe: só enxerga o que recebe. A aula Funções Locais volta a esse assunto.
O mesmo nome dentro da função local
Uma função local pode ter um parâmetro ou uma variável com o mesmo nome de uma variável do programa C# 8. Lá dentro, o nome passa a ser o dela, e a de fora fica "escondida" (em inglês, shadowing, "fazer sombra"):
string nome = "Ana";
Saudar("Bruno");
Console.WriteLine($"Fora: {nome}");
void Saudar(string nome)
{
Console.WriteLine($"Dentro: {nome}");
}
$ dotnet run sombra.cs Dentro: Bruno Fora: Ana
É permitido, mas confunde. Essa é a exceção à regra do CS0136 que você viu na seção 2: ela vale para blocos comuns, e não para funções locais. O guia prefere nomes diferentes.
Formas diferentes de compartilhar um valor entre métodos
| Jeito | Exemplo | Quando usar |
|---|---|---|
Parâmetro e return | total = Somar(total, 5); | O padrão: deixa claro o que entra e o que sai |
| Captura numa função local | vendas++; dentro de Vender() | Programas curtos de um arquivo; ajudantes que só fazem sentido ali |
ref | Vender(ref vendas); | Quando o método precisa mudar uma variável de quem chamou e você quer que isso apareça na chamada (ref, out e in) |
| Campo da classe | static int vendas; | Na forma com classe, um valor usado por vários métodos (Campos) |
Declare cada variável o mais perto possível de onde ela é usada, e no bloco mais de dentro que der. Assim, menos código consegue mexer nela, e quem lê não precisa procurar longe. Uma variável usada só dentro de um for nasce dentro do for.
Quando dá errado
error CS0103: O nome "mensagem" não existe no contexto atual
O que significa: o nome não existe naquele ponto. Ou foi digitado errado, ou a variável nasceu num bloco que já fechou (um if, um for, outro método), ou, na forma com classe, ela mora em outro método.
Como consertar: declare a variável antes do bloco, no escopo em que ela vai ser usada; ou passe o valor por parâmetro e traga de volta com return.
error CS0136: Um local ou um parâmetro denominado "total" não pode ser declarado neste escopo…
O que significa: um bloco de dentro tentou criar uma variável com o nome de uma que está viva no bloco de fora (ou de um parâmetro do método). Também acontece com for (int n = …) dentro de um método que tem o parâmetro n.
Como consertar: tire o tipo, se queria mudar a variável existente; senão, use outro nome.
error CS0165: Uso de variável local não atribuída "loja"
O que significa: a variável existe, mas, naquele ponto, pode ainda não ter valor: foi declarada sem valor e só recebe um num if sem else, ou uma função local que a usa foi chamada antes da linha que dá o valor.
Como consertar: dê um valor inicial na declaração, cubra todos os caminhos (o else), ou mova a chamada da função local para depois da linha que dá o valor.
error CS0841: Não é possível usar a variável local "preco" antes de declará-la
O que significa: a linha usa a variável antes da linha que a declara. O escopo começa na declaração.
Como consertar: mova a declaração para cima.
error CS8421: Uma função local estática não pode conter uma referência a 'loja'.
O que significa: uma função local com static tentou usar uma variável de fora.
Como consertar: passe o valor por parâmetro, ou tire o static se a captura for mesmo o que você quer.
warning CS0219: A variável "loja" é atribuída, mas seu valor nunca é usado
O que significa: a variável recebeu um valor e ninguém a leu. Junto com um CS0103 em outro método, é o sinal de que você esperava que um método de classe enxergasse uma variável do Main.
Como consertar: passe a variável como argumento para o método que precisa dela.
Referência rápida
| Onde a variável nasce | Onde ela existe |
|---|---|
| No nível do programa (instruções de nível superior) | Da declaração até o fim do arquivo, inclusive dentro das funções locais |
Dentro de um bloco { } (if, else, while…) | Da declaração até o } desse bloco |
Nos parênteses do for / foreach | Só dentro do laço |
| Parâmetro de um método | Só dentro do método |
| Dentro de um método | Só dentro do método; nasce de novo a cada chamada |
Campo da classe (static int x;) | Em todos os métodos da classe, enquanto o programa roda |
Exercícios
1. O código do erro
Qual é o código do erro "O nome … não existe no contexto atual"? (por exemplo, CS1234)
2. Quanto mostra?
Num arquivo com instruções de nível superior: int pontos = 1; Somar(); Somar(); Console.WriteLine(pontos); e, no fim, void Somar() { pontos += 10; }. O que aparece?
3. A palavra que proíbe
Que palavra, escrita antes de uma função local, proíbe que ela use as variáveis de fora?
4. Dois laços for (int i = 0; …), um depois do outro, no mesmo programa. O que acontece?
5. Na forma com classe, o Main declara string loja e chama static void Mostrar(), que usa loja. O que acontece?
6. Um método tem int vezes = 0; vezes++; e mostra vezes. Chamado três vezes, o que aparece?
Missão final: o contador de vendas
Missão: o contador vendas mora dentro do método, então recomeça do zero a cada chamada, e todas as vendas saem como "nº 1". Mude a variável de lugar para que ela dure entre as chamadas, e acrescente no fim do programa uma linha Total de vendas: 3 (que só funciona se vendas existir lá). A saída deve ser:
Vendido: Caderno (venda nº 1) Vendido: Caneta (venda nº 2) Vendido: Borracha (venda nº 3) Total de vendas: 3
Vender("Caderno");
Vender("Caneta");
Vender("Borracha");
void Vender(string produto)
{
int vendas = 0;
vendas++;
Console.WriteLine($"Vendido: {produto} (venda nº {vendas})");
}
int vendas = 0;
Vender("Caderno");
Vender("Caneta");
Vender("Borracha");
Console.WriteLine($"Total de vendas: {vendas}");
void Vender(string produto)
{
vendas++;
Console.WriteLine($"Vendido: {produto} (venda nº {vendas})");
}
Se apareceu CS0165, a linha int vendas = 0; ficou depois das chamadas: a função local usa a variável, então ela precisa ter valor antes da primeira chamada. Se o contador continuou em 1, sobrou um int vendas = 0; dentro do método: ele cria outra variável, que faz sombra à de fora (numa função local isso não dá erro, como você viu na seção 4).
Resumo
- Escopo é onde um nome existe: da declaração até o
}do bloco em que a variável nasceu. Fora disso,CS0103; antes da declaração,CS0841. - Variáveis de
if,foreforeachmorrem no fim do bloco. Blocos irmãos podem repetir nomes; um bloco de dentro não pode repetir um nome vivo lá fora (CS0136). - Variáveis e parâmetros de um método só existem dentro dele, e nascem de novo a cada chamada.
- As funções locais do fim do arquivo capturam as variáveis do programa (enxergam e mudam). Os métodos de classe, não: lá, use parâmetros ou campos.
staticnuma função local proíbe a captura (CS8421). Prefira sempre o menor escopo possível.