Um tour pela linguagem C#

O C# (pronuncia-se "C Sharp") é uma linguagem de programação moderna, orientada a objeto e fortemente tipada. O C# permite que os desenvolvedores criem muitos tipos de aplicativos seguros e robustos que são executados no .NET. O C# tem suas raízes na família de linguagens C e os programadores em C, C++, Java e JavaScript a reconhecerão imediatamente. Este tour dá uma visão geral dos principais componentes da linguagem em C# 8 e anterioe. Se quiser explorar a linguagem por meio de exemplos interativos, experimente os tutoriais de Introdução à linguagem C#.

C# é uma linguagem de programação orientada a objetos e orientada a componentes. C# fornece construções de linguagem para dar suporte diretamente a esses conceitos, tornando C# uma linguagem natural para criação e uso de componentes de software. Desde sua origem, o C# adicionou recursos para dar suporte a novas cargas de trabalho e práticas emergentes de design de software. Em sua essência, o C# é uma linguagem orientada a objeto. Você define os tipos e o comportamento deles.

Vários recursos do C# ajudam a criar aplicativos robustos e duráveis. A coleta de lixo recupera automaticamente a memória ocupada por objetos não utilizados inacessíveis. Tipos anuláveis são protegidos contra variáveis que não se referem a objetos alocados. O tratamento de exceções fornece uma abordagem estruturada e extensível para detecção e recuperação de erros. As expressões Lambda dão suporte a técnicas de programação funcional. Consulta Integrada à Linguagem (LINQ) a sintaxe cria um padrão comum para trabalhar com dados de qualquer fonte. O suporte à linguagem para operações assíncronas fornece sintaxe para a criação de sistemas distribuídos. C# tem um sistema de tipo unificado. Todos os tipos do C#, incluindo tipos primitivos, como int e double, herdam de um único tipo de object raiz. Todos os tipos compartilham um conjunto de operações comuns. Valores de qualquer tipo podem ser armazenados, transportados e operados de maneira consistente. Além disso, o C# dá suporte a tipos de referência e tipos de valor definidos pelo usuário. O C# permite a alocação dinâmica de objetos e o armazenamento em linha de estruturas leves. O C# dá suporte a métodos e tipos genéricos, que fornecem maior segurança e desempenho do tipo. O C# fornece iteradores, que permitem que os implementadores de classes de coleção definam comportamentos personalizados para o código do cliente.

O C# enfatiza o controle de versão para garantir que programas e bibliotecas possam evoluir ao longo do tempo de maneira compatível. Aspectos do design do C# que foram diretamente influenciados pelas considerações de controle de versão incluem os modificadores separados virtual e override, as regras de resolução de sobrecarga de método e suporte para declarações explícitas de membro de interface.

Arquitetura do .NET

Programas C# são executados no .NET, um sistema de execução virtual chamado CLR (Common Language Runtime) e um conjunto de bibliotecas de classes. O CLR é a implementação pela Microsoft da CLI (Common Language Infrastructure), um padrão internacional. A CLI é a base para criar ambientes de execução e desenvolvimento nos quais as linguagens e bibliotecas funcionam em conjunto perfeitamente.

O código-fonte escrito em C# é compilado em uma IL (linguagem intermediária) que está em conformidade com a especificação da CLI. O código IL e os recursos, como bitmaps e cadeias de caracteres, são armazenados em um assembly, normalmente com uma extensão de .dll. Um assembly contém um manifesto que fornece informações sobre os tipos, a versão e a cultura.

Quando o programa C# é executado, o assembly é carregado no CLR. Em seguida, o CLR executará a compilação JIT (Just-In-Time) para converter o código de IL em instruções nativas da máquina. O CLR também oferece outros serviços relacionados à coleta automática de lixo, tratamento de exceções e gerenciamento de recursos. O código que é executado pelo CLR é, às vezes, chamado de "código gerenciado". "Código não gerenciado" é compilado em linguagem de máquina nativa e visa uma plataforma específica.

Interoperabilidade de linguagem é um recurso importante do .NET. O código IL produzido pelo compilador C# está em conformidade com a CTS (Common Type Specification). O código IL gerado a partir do C# pode interagir com o código que foi gerado a partir das versões do .NET do F#, do Visual Basic, do C++. Há mais de 20 outras linguagens compatíveis com CTS. Um único assembly pode conter vários módulos gravados em diferentes idiomas do .NET. Os tipos podem fazer referência uns aos outros como se fossem escritos na mesma linguagem.

Além dos serviços de tempo de execução, o .NET também inclui bibliotecas extensas. Essas bibliotecas dão suporte a muitas cargas de trabalho diferentes. Eles são organizados em namespaces que fornecem uma ampla variedade de funcionalidades úteis. As bibliotecas incluem desde a entrada e a saída do arquivo até a manipulação de cadeia de caracteres até a análise de XML até estruturas de aplicativos Web para controles de Windows Forms. O aplicativo típico em C# usa bastante a biblioteca de classes para lidar com tarefas comuns de "conexão".

Para obter mais informações sobre o .NET, consulte Visão geral do .NET.

Hello world

O programa "Hello, World" é usado tradicionalmente para introduzir uma linguagem de programação. Este é para C#:

using System;

class Hello
{
    static void Main()
    {
        Console.WriteLine("Hello, World");
    }
}

O programa "Hello, World" começa com uma diretiva using que faz referência ao namespace System. Namespaces fornecem um meio hierárquico de organizar bibliotecas e programas em C#. Os namespaces contêm tipos e outros namespaces — por exemplo, o namespace System contém uma quantidade de tipos, como a classe Console referenciada no programa e diversos outros namespaces, como IO e Collections. A diretiva using que faz referência a um determinado namespace permite o uso não qualificado dos tipos que são membros desse namespace. Devido à diretiva using, o programa pode usar Console.WriteLine como um atalho para System.Console.WriteLine.

A classe Hello declarada pelo programa "Hello, World" tem um único membro, o método chamado Main. O método Main é declarado com o modificador static. Embora os métodos de instância possam fazer referência a uma determinada instância de objeto delimitador usando a palavra-chave this, métodos estáticos operam sem referência a um objeto específico. Por convenção, um método estático denominado Main serve como ponto de entrada de um programa C#.

A saída do programa é produzida pelo método WriteLine da classe Console no namespace System. Essa classe é fornecida pelas bibliotecas de classe padrão, que, por padrão, são referenciadas automaticamente pelo compilador.

Tipos e variáveis

Um tipo define a estrutura e o comportamento de qualquer dado em C#. A declaração de um tipo pode incluir seus membros, tipo base, interfaces implementadas e operações permitidas para esse tipo. Uma variável é um rótulo que se refere a uma instância de um tipo específico.

Há dois tipos em C#: tipos de referência e tipos de valor. Variáveis de tipos de valor contêm diretamente seus dados. Variáveis de tipos de referência armazenam referências a seus dados, o último sendo conhecido como objetos. Com tipos de referência, é possível que duas variáveis referenciem o mesmo objeto e, portanto, é possível que operações em uma variável afetem o objeto referenciado por outra variável. Com tipos de valor, cada variável tem sua própria cópia dos dados e não é possível que operações em uma variável afetem a outra (exceto em variáveis de parâmetros ref e out).

Um identificador é um nome de variável. Um identificador é uma sequência de caracteres unicode sem nenhum espaço em branco. Um identificador pode ser uma palavra reservada em C#, se for prefixado por @. Usar uma palavra reservada como identificador pode ser útil ao interagir com outros idiomas.

Os tipos de valor do C# são divididos em tipos simples, tipos de enumeração, tipos struct e tipos de valor anulável e tipos de valor de tupla. Os tipos de referência do C# são divididos em tipos de classe, tipos de interface, tipos de matriz e tipos delegados.

A estrutura de tópicos a seguir fornece uma visão geral do sistema de tipos do C#.

Os programas em C# usam declarações de tipos para criar novos tipos. Uma declaração de tipo especifica o nome e os membros do novo tipo. Seis das categorias do C# de tipos são tipos definidos pelo usuário: tipos de classe, tipos struct, tipos de interface, tipos enum, tipos delegados e tipos de valor de tupla. Você também pode declarar tipos record, record struct ou record class. Os tipos de registro têm membros sintetizados pelo compilador. Você usa registros principalmente para armazenar valores, com o mínimo de comportamento associado.

  • Um tipo class define uma estrutura de dados que contém membros de dados (campos) e membros de função (métodos, propriedades e outros). Os tipos de classe dão suporte à herança única e ao polimorfismo, mecanismos nos quais as classes derivadas podem estender e especializar as classes base.
  • Um tipo struct é semelhante a um tipo de classe que representa uma estrutura com membros de dados e membros da função. No entanto, diferentemente das classes, structs são tipos de valor e, normalmente, não precisam de alocação de heap. Os tipos de estrutura não dão suporte à herança especificada pelo usuário, e todos os tipos de structs são herdados implicitamente do tipo object.
  • Um tipo interface define um contrato como um conjunto nomeado de membros públicos. Um class ou struct que implementa um interface deve fornecer implementações de membros da interface. Um interface pode herdar de várias interfaces base e um class ou struct pode implementar várias interfaces.
  • Um tipo delegate representa referências aos métodos com uma lista de parâmetros e tipo de retorno específicos. Delegados possibilitam o tratamento de métodos como entidades que podem ser atribuídos a variáveis e passadas como parâmetros. Os delegados são análogos aos tipos de função fornecidos pelas linguagens funcionais. Eles também são semelhantes ao conceito de ponteiros de função encontrado em algumas outras linguagens. Ao contrário dos ponteiros de função, os delegados são orientados a objetos e fortemente tipados.

Os tipos class, struct, interface e delegate dão suporte a genéricos e podem ser parametrizados com outros tipos.

O C# dá suporte a matrizes unidimensionais e multidimensionais de qualquer tipo. Ao contrário dos tipos listados acima, os tipos de matriz não precisam ser declarados antes de serem usados. Em vez disso, os tipos de matriz são construídos seguindo um nome de tipo entre colchetes. Por exemplo, int[] é uma matriz unidimensional de int, int[,] é uma matriz bidimensional de int, e int[][] é uma matriz unidimensional da matriz unidimensional, ou uma matriz "denteada", de int.

Tipos anuláveis não exigem uma definição separada. Para cada tipo não nulo T há um tipo anulável correspondente T?, que pode conter um valor adicional, null. Por exemplo, int? é um tipo que pode conter qualquer inteiro de 32 bits ou o valor null e string? é um tipo que pode conter qualquer string ou o valor null.

O sistema de tipos do C# é unificado, de modo que um valor de qualquer tipo pode ser tratado como um object. Cada tipo no C#, direta ou indiretamente, deriva do tipo de classe object, e object é a classe base definitiva de todos os tipos. Os valores de tipos de referência são tratados como objetos simplesmente exibindo os valores como tipo object. Os valores de tipos de valor são tratados como objetos, executando conversão boxing e operações de conversão unboxing. No exemplo a seguir, um valor int é convertido em object e volta novamente ao int.

int i = 123;
object o = i;    // Boxing
int j = (int)o;  // Unboxing

Quando um valor de um tipo de valor é atribuído a uma referência object, uma "caixa" é alocada para conter o valor. Essa caixa é uma instância de um tipo de referência e o valor é copiado nessa caixa. Por outro lado, quando uma referência object é lançada em um tipo de valor, object é feita uma verificação de que o referenciado é uma caixa do tipo de valor correto. Se a verificação for bem-sucedida, o valor na caixa será copiado para o tipo de valor.

O sistema de tipo unificado do C# significa efetivamente que os tipos de valor são tratados como referências object "sob demanda". Devido à unificação, as bibliotecas de uso geral que usam o tipo object podem ser usadas com todos os tipos derivados de object, incluindo tipos de referência e tipos de valor.

Existem vários tipos de variáveis no C#, incluindo campos, elementos de matriz, variáveis locais e parâmetros. As variáveis representam locais de armazenamento. Cada variável tem um tipo que determina quais valores podem ser armazenados na variável, conforme mostrado abaixo.

  • Tipo de valor não nulo
    • Um valor de tipo exato
  • Tipos de valor anulável
    • Um valor null ou um valor do tipo exato
  • objeto
    • Uma referência null, uma referência a um objeto de qualquer tipo de referência ou uma referência a um valor de qualquer tipo de valor demarcado
  • Tipo de classe
    • Uma referência null, uma referência a uma instância desse tipo de classe ou uma referência a uma instância de uma classe derivada desse tipo de classe
  • Tipo de interface
    • Uma referência null, uma referência a uma instância de um tipo de classe que implementa esse tipo de interface ou uma referência a um valor demarcado de um tipo de valor que implementa esse tipo de interface
  • Tipo de matriz
    • Uma referência null, uma referência a uma instância desse tipo de matriz ou uma referência a uma instância de um tipo de matriz compatível
  • Tipo delegado
    • Uma referência null ou uma referência a uma instância de um tipo de delegado compatível

Estrutura do programa

Os principais conceitos organizacionais em C# são programas, namespaces, tipos, membros e assemblies. Os programas declaram tipos que contêm membros e podem ser organizados em namespaces. Classes, structs e interfaces são exemplos de tipos. Campos, métodos, propriedades e eventos são exemplos de membros. Quando os programas em C# são compilados, eles são empacotados fisicamente em assemblies. Os assemblies normalmente têm a extensão de arquivo .exe ou .dll, dependendo se eles implementam aplicativos ou bibliotecas, respectivamente.

Como um pequeno exemplo, considere um assembly que contém o seguinte código:

namespace Acme.Collections;

public class Stack<T>
{
    Entry _top;

    public void Push(T data)
    {
        _top = new Entry(_top, data);
    }

    public T Pop()
    {
        if (_top == null)
        {
            throw new InvalidOperationException();
        }
        T result = _top.Data;
        _top = _top.Next;

        return result;
    }

    class Entry
    {
        public Entry Next { get; set; }
        public T Data { get; set; }

        public Entry(Entry next, T data)
        {
            Next = next;
            Data = data;
        }
    }
}

O nome totalmente qualificado dessa classe é Acme.Collections.Stack. A classe contém vários membros: um campo chamado _top, dois métodos chamados Push e Pop e uma classe aninhada chamada Entry. A classe Entry ainda contém três membros: um campo chamado Next, um campo chamado Data e um construtor. Stack é uma classe genérica. Ele tem um parâmetro de tipo, T que é substituído por um tipo concreto quando é usado.

Uma pilha é uma coleção "primeiro a entrar - último a sair" (FILO). O elemento adicionado à parte superior da stack. Quando um elemento é removido, ele é removido da parte superior da pilha. O exemplo anterior declara o tipo Stack que define o armazenamento e o comportamento de uma pilha. Você pode declarar uma variável que se refere a uma instância do tipo Stack para usar essa funcionalidade.

Os assemblies contêm código executável na forma de instruções de IL (Linguagem Intermediária) e informações simbólicas na forma de metadados. Antes de ser executado, o compilador Just-In-Time (JIT) do .NET Common Language Runtime converte o código IL em um assembly em código específico do processador.

Como um assembly é uma unidade autodescritiva da funcionalidade que contém o código e os metadados, não é necessário de diretivas #include e arquivos de cabeçalho no C#. Os tipos públicos e os membros contidos em um assembly específico são disponibilizados em um programa C# simplesmente fazendo referência a esse assembly ao compilar o programa. Por exemplo, esse programa usa a classe Acme.Collections.Stack do assembly acme.dll:

class Example
{
    public static void Main()
    {
        var s = new Acme.Collections.Stack<int>();
        s.Push(1); // stack contains 1
        s.Push(10); // stack contains 1, 10
        s.Push(100); // stack contains 1, 10, 100
        Console.WriteLine(s.Pop()); // stack contains 1, 10
        Console.WriteLine(s.Pop()); // stack contains 1
        Console.WriteLine(s.Pop()); // stack is empty
    }
}

Para compilar este programa, você precisaria fazer referência ao assembly que contém a classe de pilha definida no exemplo anterior.

Os programas C# podem ser armazenados em vários arquivos de origem. Quando um programa C# é compilado, todos os arquivos de origem são processados juntos e os arquivos de origem podem se referenciar livremente. Conceitualmente, é como se todos os arquivos de origem fossem concatenados em um arquivo grande antes de serem processados. Declarações de encaminhamento nunca são necessárias em C#, porque, com poucas exceções, a ordem de declaração é insignificante. O C# não limita um arquivo de origem para declarar somente um tipo público nem requer o nome do arquivo de origem para corresponder a um tipo declarado no arquivo de origem.

Outros artigos neste tour explicam esses blocos organizacionais.