Orientação a Objetos: Novidades em relação a C

Se você vem de C, é necessário entender um pouco sobre Orientação a Objetos (OO). Em C não existe Objeto no sentido de OO, então as estruturas são agregados de informações conforme você definir (para não sair infestando o programa com variáveis em todo lugar, definindo tipos que possuem propriedades próprias, como User possuir login e email). Ou seja, são muito mais organizacionais do que exatamente feitos para interagir entre si (comparando com objetos).

Em linguagens que possuem orientação a objetos, você tem, advinhe, objetos. Objetos são diferentes (inclusive conceitualmente) de estruturas do C.

Por simplicidade, um objeto possui atributos (as variáveis internas dele), constantes internas, e métodos (funções-membro).

Uma das ideias da orientação a objetos é a troca de mensagens entre os objetos, que se dá por meio de métodos (lembre-se de que operadores, ou seja, +, -, *, /, <<, >>, <, >, <=, >=, etc., também são mensagens - menos em Java e Object-Pascal, são umas das únicas linguagens orientadas a objeto em que operadores são procedurais - ou seja, fazem apenas comparações numéricas -; definir como esses operadores funcionam se chama operator overloading).

Em C, quando você instancia uma estrutura, os dados internos dela (suas variáveis) são carregados com lixo de memória, ou seja: foi cedido um espaço de memória para eles, mas esse espaço não foi zerado, então se a última ação em um dos bytes que foram cedidos foi escrever um "5" nele, esse 5 estará lá no meio da sua instância (a menos que você use um calloc). Não há uma ideia de "o que acontece quando sou inicializado?", que é chamado de construtor. Em OO você possui construtores, para os quais você pode passar parâmetros que dirão como o objeto será inicializado. Ou seja, em C, para você inicializar uma estrutura:

typedef struct {
    int first_value;
    int second_value;
    char third_value;
} my_struct;

int main(void) {
    my_struct instance = {
        .first_value = 5,
        .second_value = -1,
        .third_value = 'a',
    };

    // ...
}

Em Orientação a Objetos, quando esses valores precisam já estar designados antes do objeto estar pronto para uso (por exemplo, já ter uma ID, e que o programador não pode esquecer de dar o valor a ela), você possui construtores.

OBS: Construtores não servem apenas para definir o valor dos atributos. Um construtor de uma janela por exemplo pode também colocar cor de fundo padrão, conectar botões às funções que serão executadas ao clicar neles, etc.

No caso de C#, my_struct poderia ser implementado da seguinte forma:

class MyStruct {
    int firstValue;
    int secondValue;
    char thirdValue;

    // Construtores são definidos como funções sem retorno e com nome igual ao
    // da classe
    public MyStruct(int firstValue, int secondValue) {
        this.firstValue = firstValue;
        this.secondValue = secondValue;
        this.thirdValue = 'a'; // Supondo que o padrão de thirdValue seja 'a'
    }
}

public class Example {
    public static void Main() {
        // `instance` é inicializada como uma MyStruct com `firstValue` = 5,
        // `secondValue` = -1 e `thirdValue` = 'a'.
        var instance = new MyStruct(5, -1);
    }
}

Perceba que, em C#, objetos são criados utilizando o operador new. Para ReferenceTypes, isso significa fazer uma alocação dinâmica (ou seja, é análogo a chamar uma função que dá calloc e inicializa os valores da struct conforme conveniente). Em outras palavras, o construtor de MyStruct seria equivalente a, em C, ter uma função de inicialização para struct my_struct:

my_struct* new_my_struct(int first_value, int second_value) {
    my_struct* obj = calloc(1, sizeof(my_struct));

    *obj = (my_struct){
        .first_value = first_value,
        .second_value = second_value,
        .third_value = 'a',
    }

    return obj;
}

C# ainda tem um "jeito C# de fazer as coisas", que nada mais é do que, em vez de utilizar um construtor para setar os valores iniciais da classe, utilizar o que se chama de Initializer List (também presente em C++).

Funciona semelhante à inicialização de structs de C:

class MyStruct {
    int firstValue;
    int secondValue;
    char thirdValue = 'a'; // É possível já dar o valor padrão de um atributo
                           // na sua inicialização
}

public class Example {
    public static void Main() {
        MyStruct instance = new MyStruct {
            firstValue = 5,   // Como em C, é "," e não ";"
            secondValue = -1,
        };
    }
}

No caso, suponha que você só dê o valor de firstValue, mas não do secondValue. Qual será o valor de secondValue? Em C ele seria inicializado com lixo da memória, já em C# ele é inicializado com o valor padrão 0. Esse "valor padrão" varia conforme o tipo de dado, conforme na tabela:

Tipo de dadoValor padrão
Inteiros (int, short, ...)0
Reais (float, double, ...)0.0
boolfalse
char'\0'
Objetosnull¹

¹: null funciona igual ao NULL e quer dizer "não há um valor sendo guardado aqui". Significa que a variável não referencia algum trecho de memória alocada. Portanto, para não ter problemas tentando acessar atributos/métodos de objetos em cima de null, sempre inicialize seus objetos! i.e. dê variable = new Algo(...)). Se você tem uma variável com valor null e que todos possam acessar, alguma coisa está errada no seu programa, pois significa que alguém está em um estado inválido.