C# static

Até aqui, cada campo e cada propriedade pertenciam a um objeto: a Ana tem o nome dela, o Bruno tem o dele. Mas algumas coisas não são de ninguém em particular: o total de alunos da escola, a fórmula que converte graus Celsius em Fahrenheit, o número π. Para elas, o C# tem a palavra static ("estático"): o membro passa a pertencer à classe, e existe uma vez só, não importa quantos objetos você crie.

Você vai aprender
  • A diferença entre membro de instância (de cada objeto) e membro static (da classe).
  • Como contar objetos criados com um campo ou uma propriedade static.
  • Que, em C#, um membro static se usa só pelo nome da classe (CS0176).
  • Métodos static, como Math.Max e Console.WriteLine, e como criar os seus.
  • O que um método static não pode usar (CS0120).
  • Classes static (CS0712, CS0708), o construtor static e o using static.
  • Por que o Main é static.

A ideia: o caderno de cada aluno e o quadro da sala

Numa sala de aula, cada aluno tem o seu caderno. O que a Ana escreve no caderno dela não aparece no caderno do Bruno. Mas a sala tem um quadro só, na parede. Quando a professora escreve "prova na sexta" no quadro, todo mundo vê a mesma coisa, e ninguém precisa ter uma cópia.

  • Os cadernos são os membros de instância: cada objeto tem os seus (Nome, Nota).
  • O quadro é um membro static: pertence à classe, existe uma vez só e é o mesmo para todos.

Uma instância é um objeto criado com new. "Membro de instância" quer dizer "membro que cada objeto tem o seu".


O contador de alunos

A Ana quer saber quantos alunos foram matriculados. Primeiro, a tentativa ingênua: uma propriedade comum, que cada aluno aumenta no construtor.

var ana = new Aluno("Ana");
var bruno = new Aluno("Bruno");
var carla = new Aluno("Carla");

Console.WriteLine($"Ana conta: {ana.Total}");
Console.WriteLine($"Carla conta: {carla.Total}");

class Aluno
{
    public int Total { get; private set; }

    public Aluno(string nome)
    {
        Nome = nome;
        Total++;
    }

    public string Nome { get; }
}
$ dotnet run sem-static.cs
Ana conta: 1
Carla conta: 1          « cada aluno contou só a si mesmo

Cada objeto tem o seu Total, que começa em 0 e vai para 1. É como se cada aluno anotasse "1 aluno" no próprio caderno. Para ter um número só, o total tem de ir para o quadro: static.

var ana = new Aluno("Ana");
var bruno = new Aluno("Bruno");
var carla = new Aluno("Carla");

Console.WriteLine(ana.Nome);
Console.WriteLine(carla.Nome);
Console.WriteLine($"Alunos matriculados: {Aluno.Total}");

class Aluno
{
    public static int Total { get; private set; }

    public Aluno(string nome)
    {
        Nome = nome;
        Total++;
    }

    public string Nome { get; }
}
$ dotnet run contador.cs
Ana
Carla
Alunos matriculados: 3
public static int Total { get; private set; }
Com static, existe um Total para a classe inteira, criado antes de qualquer aluno. Ele começa em 0.
Total++; no construtor
Cada new Aluno(…) soma 1 no mesmo Total. Depois de três alunos, ele vale 3.
public string Nome { get; }
Sem static: cada aluno tem o seu nome. Isso continua igual.
Aluno.Total
Um membro static se usa pelo nome da classe, e não por um objeto. É como ler o quadro da sala: você não precisa pegar o caderno de ninguém.

Pelo objeto não dá

Em algumas linguagens (como Java), dá para escrever ana.Total mesmo com Total sendo static. Em C#, não: o compilador exige o nome da classe, para ninguém achar que o total é "da Ana".

var ana = new Aluno("Ana");
Console.WriteLine(ana.Total);

class Aluno
{
    public static int Total { get; private set; }

    public Aluno(string nome)
    {
        Nome = nome;
        Total++;
    }

    public string Nome { get; }
}
$ dotnet run pelo-objeto.cs
/home/ana/cs/pelo-objeto.cs(2,19): error CS0176: O membro "Aluno.Total" não pode ser acessado com uma referência de instância; qualifique-o com um nome de tipo

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

"Qualifique-o com um nome de tipo" quer dizer: escreva o nome da classe na frente, Aluno.Total. Dentro da própria classe (como no construtor), o nome sozinho, Total, basta.

Missão: a padaria da Ana numera os pedidos, mas todos estão saindo com o número 1. Rode e veja. Depois, deixe o campo _contador static, para que ele seja um só para todos os pedidos, e rode de novo.

var p1 = new Pedido("pão");
var p2 = new Pedido("leite");
var p3 = new Pedido("café");
Console.WriteLine($"Pedido {p1.Numero}: {p1.Item}");
Console.WriteLine($"Pedido {p2.Numero}: {p2.Item}");
Console.WriteLine($"Pedido {p3.Numero}: {p3.Item}");

class Pedido
{
    private int _contador;

    public Pedido(string item)
    {
        _contador++;
        Numero = _contador;
        Item = item;
    }

    public int Numero { get; }
    public string Item { get; }
}
var p1 = new Pedido("pão");
var p2 = new Pedido("leite");
var p3 = new Pedido("café");
Console.WriteLine($"Pedido {p1.Numero}: {p1.Item}");
Console.WriteLine($"Pedido {p2.Numero}: {p2.Item}");
Console.WriteLine($"Pedido {p3.Numero}: {p3.Item}");

class Pedido
{
    private static int _contador;

    public Pedido(string item)
    {
        _contador++;
        Numero = _contador;
        Item = item;
    }

    public int Numero { get; }
    public string Item { get; }
}

Repare na mistura: _contador é do quadro (um só), e Numero é do caderno (cada pedido guarda o número que recebeu). É um padrão muito comum para dar números de série a objetos.


Métodos static

Você usa métodos static desde a primeira aula. Console.WriteLine é um: você nunca escreveu new Console(). Math.Max e int.Parse também. Eles são da classe porque não dependem de nenhum objeto: para achar o maior entre 3 e 8, ninguém precisa de "uma matemática" criada com new.

Criando os seus

A Ana quer converter temperaturas. A conta só depende do número que entra; não há nada para guardar num objeto. É um caso perfeito para static:

Console.WriteLine(Conversor.CelsiusParaFahrenheit(25));
Console.WriteLine(Conversor.CelsiusParaFahrenheit(-10));
Console.WriteLine(Math.Max(3, 8));

static class Conversor
{
    public static double CelsiusParaFahrenheit(double celsius)
    {
        return celsius * 9 / 5 + 32;
    }
}
$ dotnet run conversor.cs
77
14
8
public static double CelsiusParaFahrenheit(double celsius)
Um método static: chama-se pela classe, Conversor.CelsiusParaFahrenheit(25), sem criar objeto.
static class Conversor
Uma classe static: ela só pode ter membros static e nunca vira objeto. É uma "caixa de ferramentas". A classe Math do .NET é assim.

O que uma classe static não deixa fazer

Como ela nunca vira objeto, new é proibido:

$ dotnet run new-static.cs          « com var c = new Conversor();
/home/ana/cs/new-static.cs(1,9): error CS0712: Não é possível criar uma instância da classe estática "Conversor"
/home/ana/cs/new-static.cs(1,1): error CS0723: Não é possível declarar uma variável de tipo static "Conversor"

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

E um membro sem static dentro dela também:

Console.WriteLine(Conversor.Dobro(4));

static class Conversor
{
    public static int Dobro(int n)
    {
        return n * 2;
    }

    public int Triplo(int n)
    {
        return n * 3;
    }
}
$ dotnet run membro-instancia.cs
/home/ana/cs/membro-instancia.cs(10,16): error CS0708: "Triplo": não pode declarar membros de instância em uma classe estática

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O Triplo seria "do caderno", mas uma classe static não tem cadernos. Conserto: public static int Triplo.


O que um método static não pode usar

Um método static roda sem objeto nenhum. Por isso, lá dentro, não existe this e não existem os campos de instância: de qual objeto ele leria?

Console.WriteLine(Loja.Resumo());

class Loja
{
    private string _nome = "Lojinha da Ana";

    public static string Resumo()
    {
        return $"Bem-vindo à {_nome}";
    }
}
$ dotnet run cs0120.cs
/home/ana/cs/cs0120.cs(9,31): error CS0120: Uma referência de objeto é necessária para o campo, o método ou a propriedade "Loja._nome" não estática

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

"Uma referência de objeto é necessária": para ler _nome, é preciso um objeto Loja, e o método static não tem nenhum. Há dois consertos, e a escolha depende do que o dado é:

  • Se o nome é de cada loja (a Ana pode ter duas lojas), tire o static do método e chame por um objeto: new Loja().Resumo().
  • Se o nome é um só para o programa inteiro, deixe o campo static também: private static string _nome = "…";.

O caminho contrário sempre funciona: um método de instância pode usar membros static à vontade (o construtor do Aluno usou o Total). Todo aluno enxerga o quadro.

De dentro de…Usa membro de instância?Usa membro static?
Método de instância (sem static)SimSim
Método staticNão (CS0120), só por um objetoSim

Por que o Main é static

Quando um programa começa, ainda não existe objeto nenhum. Alguém precisa rodar primeiro, sem depender de um new. Por isso, no formato clássico (veja Program.cs e o Main), o ponto de partida é um método static:

class Program
{
    static void Main()
    {
        Console.WriteLine(Dobro(21));
    }

    static int Dobro(int n)
    {
        return n * 2;
    }
}
$ dotnet run main.cs
42

Esquecendo o static do Dobro, o Main (que é static) não consegue chamá-lo, com o mesmo erro da seção anterior:

$ dotnet run main-erro.cs
/home/ana/cs/main-erro.cs(5,27): error CS0120: Uma referência de objeto é necessária para o campo, o método ou a propriedade "Program.Dobro(int)" não estática

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

Esse é um tropeço clássico de quem vem dos programas de um arquivo só, em que as funções escritas no topo do arquivo funcionam sem static nenhum. Por baixo dos panos, o compilador põe essas instruções de nível superior dentro de um Main static para você.


O construtor static

Às vezes um dado static precisa ser preparado antes do primeiro uso: ler uma configuração, montar uma tabela. Para isso existe o construtor static: ele roda uma vez só, sozinho, logo antes da primeira vez que a classe é usada.

Console.WriteLine("Começou o programa");
Console.WriteLine($"Taxa: {Loja.Taxa}");
Console.WriteLine($"Taxa de novo: {Loja.Taxa}");

class Loja
{
    public static decimal Taxa { get; }

    static Loja()
    {
        Console.WriteLine("(construtor static da Loja rodou)");
        Taxa = 0.05m;
    }
}
$ dotnet run construtor-static.cs
Começou o programa
(construtor static da Loja rodou)
Taxa: 0,05
Taxa de novo: 0,05          « na segunda vez, ele não roda de novo
static Loja()
O nome da classe, sem modificador de acesso e sem parâmetros. Ninguém o chama: o .NET chama sozinho.
A ordem da saída
"Começou o programa" vem antes: o construtor static só roda quando a Loja é usada pela primeira vez, e nunca mais.

using static: sem repetir o nome da classe

Se um arquivo usa muito uma classe static, dá para "importar" os membros dela e escrever só o nome do método:

using static System.Math;

Console.WriteLine(Max(3, 8));
Console.WriteLine(Sqrt(81));
Console.WriteLine(PI);
$ dotnet run using-static.cs
8
9
3,141592653589793

É cômodo em contas com muita matemática. Use com moderação: num arquivo grande, Max(…) sozinho deixa o leitor sem saber de onde aquilo veio.

E o const? Uma constante (const) já é static por natureza: Math.PI é uma const da classe Math, e você a lê pelo nome da classe. Para um valor da classe que é calculado quando o programa roda, use static readonly (veja readonly e init).

Quando usar static

Situaçãostatic?Exemplo
O dado é diferente para cada objetoNãoNome, Nota, Saldo
O dado é um só para a classe inteiraSimAluno.Total, um contador de pedidos
O método só usa os parâmetros, nada do objetoSimConversor.CelsiusParaFahrenheit, Math.Max
O método lê ou muda os campos do objetoNãoconta.Depositar(…)
Uma "caixa de ferramentas" sem dadosstatic classMath, Conversor
Cuidado com dados static que mudam. Um campo static que qualquer parte do programa altera é, na prática, uma variável global: difícil de testar e de entender. Prefira static para contas (métodos) e para valores que não mudam. Contadores como o Total funcionam bem com private set, para que só a classe mude.

Quando dá errado

error CS0176: O membro "…" não pode ser acessado com uma referência de instância; qualifique-o com um nome de tipo

O que aconteceu: você usou um membro static por um objeto (ana.Total).

Como resolver: use o nome da classe: Aluno.Total.

error CS0120: Uma referência de objeto é necessária para o campo, o método ou a propriedade "…" não estática

O que aconteceu: um método static (às vezes o Main) tentou usar um membro de instância. Também acontece ao escrever Aluno.Nome, pelo nome da classe, quando Nome é de cada aluno.

Como resolver: use um objeto (ana.Nome, new Loja().Resumo()) ou, se o dado é mesmo da classe, deixe-o static.

error CS0712: Não é possível criar uma instância da classe estática "…"

O que aconteceu: new numa static class. O CS0723 vem junto, por causa da variável.

Como resolver: não crie objeto: chame direto, Conversor.CelsiusParaFahrenheit(25).

error CS0708: "…": não pode declarar membros de instância em uma classe estática

O que aconteceu: um método ou campo sem static dentro de uma static class.

Como resolver: escreva static nele ou, se a classe precisa mesmo de objetos, tire o static da classe.


Referência

EscritaSignificado
private static int _contador;Campo da classe: um só, para todos os objetos
public static int Total { get; private set; }Propriedade da classe
public static double Dobro(double n)Método da classe; chama-se Classe.Dobro(…)
static class ConversorClasse só com membros static; não aceita new
static Loja() { … }Construtor static: roda uma vez, antes do primeiro uso
using static System.Math;Usa Max, Sqrt, PI sem escrever Math.
Aluno.TotalAcesso a membro static: sempre pelo nome da classe

Missão final: a calculadora de descontos

Missão: complete a classe static class Preco com o método public static decimal ComDesconto(decimal valor, int porcento), que devolve o valor com o desconto aplicado: valor * (100 - porcento) / 100. O programa de cima já chama o método pelo nome da classe.

Console.WriteLine(Preco.ComDesconto(100m, 10).ToString("C"));
Console.WriteLine(Preco.ComDesconto(59.90m, 50).ToString("C"));

static class Preco
{
    // escreva o método aqui
}
Console.WriteLine(Preco.ComDesconto(100m, 10).ToString("C"));
Console.WriteLine(Preco.ComDesconto(59.90m, 50).ToString("C"));

static class Preco
{
    public static decimal ComDesconto(decimal valor, int porcento)
    {
        return valor * (100 - porcento) / 100;
    }
}

Exercícios

1. Da classe

Qual palavra faz um campo pertencer à classe, e não a cada objeto?

2. Chamar pela classe

A classe Conversor tem o método static Dobro(int n). Escreva a chamada que dobra o 7 (sem o ponto e vírgula).

3. O erro do objeto

Qual é o código do erro ao usar um membro static por um objeto, como em ana.Total?

4. Três objetos Aluno foram criados, e cada construtor fez Total++ numa propriedade sem static. Quanto vale o Total de cada um?

5. Um método static tenta ler um campo de instância. O que acontece?

6. Quando roda o construtor static de uma classe?


Resumo

  • Membro de instância: cada objeto tem o seu (o caderno). Membro static: um só, da classe (o quadro).
  • Membros static se usam pelo nome da classe (Aluno.Total, Math.Max). Pelo objeto, é CS0176.
  • Método static não enxerga campos de instância nem this (CS0120). O contrário sempre funciona.
  • static class é uma caixa de ferramentas: só membros static, sem new (CS0712, CS0708).
  • O construtor static roda uma vez, antes do primeiro uso. using static dispensa o nome da classe.
  • O Main é static porque roda antes de existir qualquer objeto.