C

Tuplas no C# 7

Há pouco tempo saiu a versão 6 do C#. Nessa versão, as maiores mudanças ocorreram nos bastidores, foi a partir dela que o .NET passou a ser open source. Assim, pode parecer estranho falar sobre a versão 7 da linguagem, mas como os desenvolvedores não param, uma nova versão está por vir.

Desta forma, neste artigo vou falar de um dos recursos que será adicionado na linguagem que mais está me interessando, que são as tuplas (ou tuples).

Curso de
CONHEÇA O CURSO

O que são tuplas?

Caso programe em outra linguagem, já deve se deparado com este conceito, já que tuplas não são novidades na programação.

Podemos definir as tuplas como um conjunto temporário de valores. Você pode compará-las com uma classe POCO simples, só que no lugar de criar toda a estrutura da classe, as tuplas podem ser declaradas de forma simples e rápida.

Então, no lugar de definir uma classe assim:

class Counter
{
    public int Sum {get; set;}
    public int Count {get; set;}
}

var res = new Counter { Sum = 0, Count = 0};

Isso poderia ser definido com uma tupla da seguinte maneira:

var res = (sum: 0, count: 0);

Quando utilizá-las?

Mesmo que exemplo acima mostre uma tupla inline, este recurso geralmente será utilizado como tipo de retorno de um método.

Assim como eu, você deve ter passado pela situação onde necessitava retornar mais de um valor em um método. Hoje para fazer isso temos algumas alternativas:

  • Parâmetros de saída:
public void GetCounter(IIEnumerator<int> list, out int sum, out
int count) { ... }

int sum, count;

GetCounter(list, out sum, out count);
Console.WriteLine($"Sum: {sum}, Count: {count}");

A desvantagem é que os parâmetros de saída não funcionam com métodos assíncronos.

  • A classe System.Tulpe<T, T, ...>:
public Tuple<int, int> GetCounter(IEnumerator<int> list)
{ ... }

var counter = GetCounter(list);
Console.WriteLine($"Sum: {counter.Item1}, Count: {counter.Item2}");

A classe Tuple não tem a desvantagem dos parâmetros de saída, mas requer muita escrita e propriedades como Item1 e Item2 e isso tira um pouco da legibilidade do código.

  • Definir uma classe/struct ou tipo anônimo:
struct Counter { public int Sum; public int Count;}

public Counter GetCounter(IEnumerator<int> list) { ... }

var counter = GetCounter(list);
Console.WriteLine($"Sum: {counter.Sum}, Count: {counter.Counter}");

Esta situação não tem as desvantagens das soluções anteriores, mas gera uma sobrecarga de código desnecessária.

Assim, no C# a melhor solução será com tuplas.

  • Utilizando tuplas:
public (int sum, int count) GetCounter(IEnumerator<int> list)
{ ... }

var counter = GetCounter(list);
Console.WriteLine($"Sum: {counter.Sum}, Count: {counter.Counter}");

Note que no código acima, o retorno do método foi obtido normalmente, e que a partir dele foi obtido cada um dos valores de retorno.

Acima cada propriedade da tupla só foi reconhecida porque no método elas foram especificadas:

public (int sum, int count) GetCounter(IEnumerator<int> list)
{
    int s=0, c=0;
    foreach (var v in list) { s += v; c++; }
    return (s, c);
}

Mas isso não é algo obrigatório:

public (int, int) GetCounter(IEnumerator<int> list) 
{
    int s=0, c=0;
    foreach (var v in list) { s += v; c++; }
    return (s, c);
}

Assim, as propriedades da tupla poderiam ser acessadas com as propriedades Item1, Item2, …, ItemN:

var counter = GetCounter(list);
Console.WriteLine($"Sum: {counter.Item1}, Count: {counter.Item2}");

Mas isso não é algo que eu encorajo. Então o autocomplete nem irá mostrar este tipo de opção.

Desconstrução da tupla

Como a tupla será geralmente utilizada para se retornar mais de um valor de um método, por que continuar utilizando-a depois se se obter o retorno?

Claro que poderíamos atribuir os valores de cada propriedade da tupla para uma variável e trabalhar com esses valores separadamente. Para evitar ter todo este trabalho, o C# 7 terá a opção de desconstruir a tupla:

(var sum, var count) = GetCounter(list);
Console.WriteLine($"Sum: {sum}, Count: {count}");

Assim, as propriedades da tupla já serão atribuídas para as variáveis. Note que pode ser utilizado o var em cada variável, ou mesmo fora dos parentes:

var (sum, count) = GetCounter(list);
Console.WriteLine($"Sum: {sum}, Count: {count}");

O resultado será o mesmo.

Também pode ser definido o tipo primitivo:

(int sum, int count) = GetCounter(list);
Console.WriteLine($"Sum: {sum}, Count: {count}");

Ou mesmo declarar as variáveis fora e definí-las nos parênteses:

int sum, count;

(sum, count) = GetCounter(list);
Console.WriteLine($"Sum: {sum}, Count: {count}");

Desta desconstrução não estará disponível apenas para as tuplas. Qualquer tipo de dado pode ser desconstruído, desde que ele implemente um método desconstrutor, com a sintaxe abaixo:

public void Deconstruct(out T1 x1, ..., out Tn xn) { ... }

Assim, quando o objeto for desconstruído, este método será chamado:

class Point
{
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y) { X = x; Y = y; }

    public void Deconstruct(out int x, out int y) { x = X; y = Y; }
}

(var myX, var myY) = GetPoint(); // calls Deconstruct(out myX, out myY);

Conclusão

As tuplas são um recurso muito bem-vindo para a linguagem, facilitará o desenvolvimento e com o tempo serão muito utilizadas.

Curso de
CONHEÇA O CURSO

Conheça os principais algoritmos de ordenação

Imagine como seria buscar um número em um catálogo telefônico se os nomes das pessoas não estivessem listados em ordem alfabética? Seria muito complicado. A ordenação ou classificação de registros consiste em organizá-los em ordem crescente ou decrescente e assim facilitar a recuperação desses dados. A ordenação tem como objetivo facilitar as buscas e pesquisas de ocorrências de determinado elemento em um conjunto ordenado.

Como preza a estratégia algorítmica: “Primeiro coloque os números em ordem. Depois decidimos o que fazer.”

Na computação existe uma série de algoritmos que utilizam diferentes técnicas de ordenação para organizar um conjunto de dados, eles são conhecidos como Métodos de Ordenação ou Algoritmos de Ordenação. Vamos conhecer um pouco mais sobre eles.

Os métodos de ordenação se classificam em:

  • Ordenação Interna: onde todos os elementos a serem ordenados cabem na memória principal e qualquer registro pode ser imediatamente acessado.

  • Ordenação Externa: onde os elementos a serem ordenados não cabem na memória principal e os registros são acessados sequencialmente ou em grandes blocos.

Hoje veremos apenas os métodos de ordenação interna.

Dentro da ordenação interna temos os Métodos Simples e os Métodos Eficientes:

Curso de
CONHEÇA O CURSO

Métodos Simples

Os métodos simples são adequados para pequenos vetores, são programas pequenos e fáceis de entender. Possuem complexidade C(n) = O(n²), ou seja, requerem O(n²) comparações. Exemplos: Insertion Sort, Selection Sort, Bubble Sort, Comb Sort.

Dica: Veja uma breve introdução à análise de algoritmos

Nos algoritmos de ordenação as medidas de complexidade relevantes são:

  • Número de comparações C(n) entre chaves.
  • Número de movimentações M(n) dos registros dos vetores.

Onde n é o número de registros.

Insertion Sort

Insertion Sort ou ordenação por inserção é o método que percorre um vetor de elementos da esquerda para a direita e à medida que avança vai ordenando os elementos à esquerda. Possui complexidade C(n) = O(n) no melhor caso e C(n) = O(n²) no caso médio e pior caso. É considerado um método de ordenação estável.

Um método de ordenação é estável se a ordem relativa dos itens iguais não se altera durante a ordenação.

O funcionamento do algoritmo é bem simples: consiste em cada passo a partir do segundo elemento selecionar o próximo item da sequência e colocá-lo no local apropriado de acordo com o critério de ordenação.


(Imagens: Wikipedia.org)

Vejamos a implementação:

void insercao (int vet, int tam){
int i, j, x;
for (i=2; i<=tam; i++){
    x = vet[i];
    j=i-1;
    vet[0] = x; 
    while (x < vet[j]){
        vet[j+1] = vet[j];
        j--;
    }
    vet[j+1] = x;
}

Para compreender melhor o funcionamento do algoritmo veja este vídeo:

Selection Sort

A ordenação por seleção ou selection sort consiste em selecionar o menor item e colocar na primeira posição, selecionar o segundo menor item e colocar na segunda posição, segue estes passos até que reste um único elemento. Para todos os casos (melhor, médio e pior caso) possui complexidade C(n) = O(n²) e não é um algoritmo estável.


(Imagens: Wikipedia.org)

Veja este vídeo para conhecer o passo a passo de execução do algoritmo:

void selecao (int vet, int tam){
    int i, j, min, x;
    for (i=1; i<=n-1; i++){
        min = i;
    for (j=i+1; j<=n; j++){
            if (vet[j] < vet[min])
            min = j;
    }
    x = vet[min];
    vet[min] = vet[i];
    vet[i] = x;
    }
}

Métodos Eficientes

Os métodos eficientes são mais complexos nos detalhes, requerem um número menor de comparações. São projetados para trabalhar com uma quantidade maior de dados e possuem complexidade C(n) = O(n log n). Exemplos: Quick sort, Merge sort, Shell sort, Heap sort, Radix sort, Gnome sort, Count sort, Bucket sort, Cocktail sort, Timsort.

Quick Sort

O Algoritmo Quicksort, criado por C. A. R. Hoare em 1960, é o método de ordenação interna mais rápido que se conhece para uma ampla variedade de situações.

Provavelmente é o mais utilizado. Possui complexidade C(n) = O(n²) no pior caso e C(n) = O(n log n) no melhor e médio caso e não é um algoritmo estável.

É um algoritmo de comparação que emprega a estratégia de “divisão e conquista”. A ideia básica é dividir o problema de ordenar um conjunto com n itens em dois problemas menores. Os problemas menores são ordenados independentemente e os resultados são combinados para produzir a solução final.

Basicamente a operação do algoritmo pode ser resumida na seguinte estratégia: divide sua lista de entrada em duas sub-listas a partir de um pivô, para em seguida realizar o mesmo procedimento nas duas listas menores até uma lista unitária.

Funcionamento do algoritmo:

  • Escolhe um elemento da lista chamado pivô.
  • Reorganiza a lista de forma que os elementos menores que o pivô fiquem de um lado, e os maiores fiquem de outro. Esta operação é chamada de “particionamento”.
  • Recursivamente ordena a sub-lista abaixo e acima do pivô.

Dica: Conheça um pouco sobre algoritmos recursivos

(Imagem: Wikipedia.org)

Veja este vídeo para conhecer o passo a passo de execução do algoritmo:

void quick(int vet[], int esq, int dir){
    int pivo = esq, i,ch,j;         
    for(i=esq+1;i<=dir;i++){        
        j = i;                      
        if(vet[j] < vet[pivo]){     
            ch = vet[j];               
            while(j > pivo){           
                vet[j] = vet[j-1];      
                j--;                    
            }
            vet[j] = ch;               
            pivo++;                    
        }
    }
    if(pivo-1 >= esq){              
        quick(vet,esq,pivo-1);      
    }
    if(pivo+1 <= dir){              
        quick(vet,pivo+1,dir);      
    }
 }

A principal desvantagem deste método é que ele possui uma implementação difícil e delicada, um pequeno engano pode gerar efeitos inesperados para determinadas entradas de dados.

Mergesort

Criado em 1945 pelo matemático americano John Von Neumann o Mergesort é um exemplo de algoritmo de ordenação que faz uso da estratégia “dividir para conquistar” para resolver problemas. É um método estável e possui complexidade C(n) = O(n log n) para todos os casos.

Esse algoritmo divide o problema em pedaços menores, resolve cada pedaço e depois junta (merge) os resultados. O vetor será dividido em duas partes iguais, que serão cada uma divididas em duas partes, e assim até ficar um ou dois elementos cuja ordenação é trivial.

Para juntar as partes ordenadas os dois elementos de cada parte são separados e o menor deles é selecionado e retirado de sua parte. Em seguida os menores entre os restantes são comparados e assim se prossegue até juntar as partes.


(Imagens: Wikipedia.org)

Veja o vídeo para conhecer o passo a passo da execução do algoritmo:

void mergeSort(int *vetor, int posicaoInicio, int posicaoFim) {
    int i, j, k, metadeTamanho, *vetorTemp;
    if(posicaoInicio == posicaoFim) return;
    metadeTamanho = (posicaoInicio + posicaoFim ) / 2;

    mergeSort(vetor, posicaoInicio, metadeTamanho);
    mergeSort(vetor, metadeTamanho + 1, posicaoFim);

    i = posicaoInicio;
    j = metadeTamanho + 1;
    k = 0;
    vetorTemp = (int *) malloc(sizeof(int) * (posicaoFim - posicaoInicio + 1));

    while(i < metadeTamanho + 1 || j  < posicaoFim + 1) {
        if (i == metadeTamanho + 1 ) { 
            vetorTemp[k] = vetor[j];
            j++;
            k++;
        }
        else {
            if (j == posicaoFim + 1) {
                vetorTemp[k] = vetor[i];
                i++;
                k++;
            }
            else {
                if (vetor[i] < vetor[j]) {
                    vetorTemp[k] = vetor[i];
                    i++;
                    k++;
                }
                else {
                    vetorTemp[k] = vetor[j];
                    j++;
                    k++;
                }
            }
        }

    }
    for(i = posicaoInicio; i <= posicaoFim; i++) {
        vetor[i] = vetorTemp[i - posicaoInicio];
    }
    free(vetorTemp);
}

Shell Sort

Criado por Donald Shell em 1959, o método Shell Sort é uma extensão do algoritmo de ordenação por inserção. Ele permite a troca de registros distantes um do outro, diferente do algoritmo de ordenação por inserção que possui a troca de itens adjacentes para determinar o ponto de inserção. A complexidade do algoritmo é desconhecida, ninguém ainda foi capaz de encontrar uma fórmula fechada para sua função de complexidade e o método não é estável.

Os itens separados de h posições (itens distantes) são ordenados: o elemento na posição x é comparado e trocado (caso satisfaça a condição de ordenação) com o elemento na posição x-h. Este processo repete até h=1, quando esta condição é satisfeita o algoritmo é equivalente ao método de inserção.

A escolha do salto h pode ser qualquer sequência terminando com h=1. Um exemplo é a sequencia abaixo:

h(s) = 1, para s = 1
h(s) = 3h(s - 1) + 1, para s > 1

A sequência corresponde a 1, 4, 13, 40, 121, …

Knuth (1973) mostrou experimentalmente que esta sequencia é difícil de ser batida por mais de 20% em eficiência.

Veja o vídeo que demonstra o passo a passo da execução do algoritmo:

void shellSort(int *vet, int size) {
    int i , j , value;
    int gap = 1;
    while(gap < size) {
        gap = 3*gap+1;
    }
    while ( gap > 1) {
        gap /= 3;
        for(i = gap; i < size; i++) {
            value = vet[i];
            j = i - gap;
            while (j >= 0 && value < vet[j]) {
                vet [j + gap] = vet[j];
                j -= gap;
            }
            vet [j + gap] = value;
        }
    }
}

Conclusão

Neste artigo foi apresentado os principais métodos de ordenação com foco no conceito e funcionamento de cada um deles.

Você pode ver na tabela abaixo a comparação entre eles:

(Imagem: DECOM – UFOP)

Um abraço e até a próxima!

Curso de
CONHEÇA O CURSO

Desmistificando os algoritmos recursivos

Os algoritmos recursivos são fundamentais na solução de muitos problemas envolvendo a computação, ainda assim, muitos programadores os veem como algo complexo e de difícil implementação.

Como disse o prof. Siang Wun Song:

Para fazer um procedimento recursivo é preciso ter fé.

Calma! Apesar da fala do prof. Siang, implementar um algoritmo recursivo não é um bicho de sete cabeças. Veremos alguns passos para compreendê-los de vez. o/

Curso de
CONHEÇA O CURSO

Primeiro devemos entender o que é a recursividade. Uma função recursiva chama a si mesma dentro do próprio escopo. Pode ser uma recursão direta onde uma função A chama a própria função A ou uma recursão indireta onde uma função A chama uma função B que por sua vez chama a função A. Além de chamar a si mesma, a função recursiva deve possuir uma condição de parada que a impedirá de entrar em loop infinito.

Antes de criar uma função recursiva para determinado problema, é preciso saber se ele possui uma estrutura recursiva. Neste caso, devemos verificar se parte do problema possui o mesmo tipo do original, assim podemos dizer que a solução para ele é a recursividade. Para compreender melhor, vamos analisar o caso do somatório onde temos um número natural n >= 0 e queremos descobrir a soma de todos os números de n até 0.

Antes de criar o algoritmo devemos extrair dois elementos do problema: O caso base que se tornará a condição de parada e o passo recursivo. Vejamos algumas possíveis iterações do problema:

No caso do somatório, é possível identificar que para cada valor de n o último elemento sempre é 0, assim podemos confirmar que este é o caso base.

Analisando um pouco mais as iterações temos que para cada valor de n vamos diminuindo em 1 até que chegue ao valor 0.

Assim, nós temos que o somatório de um número natural n é o próprio n adicionado ao somatório do número antecessor (Somatório de n = n + (n-1) + (n-2) + ... + 0). Logo, o nosso passo recursivo é n + soma(n-1).

Sabemos que o caso base é 0 e o somatório de 0 é ele mesmo, estão essa será a nossa condição de parada. O nosso passo recurso é n + soma(n-1), assim já temos os elementos da nossa função recursiva e podemos criá-la:

int soma(int n){
    if(n == 0)
        return 0;
    return n + soma(n-1);
}

Esta é a implementação em C para resolver o problema de forma recursiva. Para um número natural n informado, a função irá executar até que seja encontrado o caso base e assim retornar o valor do somatório.

Vamos analisar outro problema clássico que pode ser resolvido com recursividade: o fatorial de um número natural n >= 1. O fatorial é bem semelhante ao somatório, porém é feito com multiplicação sucessiva.

Vamos analisar as possíveis iterações:

No exemplo do fatorial, o nosso caso base é 1, portanto, a nossa condição de parada será verificar se o número é 1 e, caso seja, a função deverá retorná-lo. Também é possível definir o nosso passo recursivo: fatorial de n = n x (n - 1) x (n - 2) x ... x 1 logo n! = n x fatorial(n - 1). Por fim, chegamos à seguinte implementação em C:

int fatorial(int n){
    if(n == 1)
        return 1;
    return n * fatorial(n-1);
}

Uma grande vantagem da recursividade é o fato de gerar uma redução no tamanho do algoritmo, permitindo descrevê-lo de forma mais clara e concisa. Porém, todo cuidado é pouco ao se fazer módulos recursivos. Basta seguir estes passos e você não terá grandes problemas. Como disse L. Peter Deutsch:

Iterar é humano, fazer recursão é divino.

Um abraço e até a próxima!

Curso de
CONHEÇA O CURSO

Processo de execução de um código no .NET Framework

Quando estamos desenvolvendo, o único momento em que pensamos na compilação do código é quando algum erro ocorre. Fora isso, essa é só mais uma etapa do processo de execução (que costuma passar despercebida).

Só que um bom programador precisa entender como este processo funciona, para criar códigos melhores, entender mais a linguagem que se está utilizando e compreender melhor os erros gerados.

No geral as linguagens possuem um processo de compilação parecido, mas aqui veremos o processo de compilação de uma aplicação desenvolvida para o .NET Framework.

Nele, separamos o processo de compilação em quatro etapas:

  • Escolhendo um compilador;
  • Compilando o código para a linguagem intermediária;
  • Compilando o código da linguagem intermediário para a nativa;
  • Executando o código.

Essas etapas são ilustradas na imagem abaixo:

Curso de
CONHEÇA O CURSO

Escolhendo um compilador

O .NET Framework possui um importante recurso que permite a interoperabilidade das linguagens suportadas por ele, que é o Common Language Runtime
(CLR).

Seguindo as especificações definidas no padrão ECMA-335 (Obs: Esse link leva a um arquivo PDF) – que define as especificações do CLR – qualquer linguagem pode definir um compilador suportado pelo .NET. Com isso, é possível desenvolver aplicações para o .NET utilizando C#, F#, Perl, Cobol, Ruby etc.

Assim, na compilação, a primeira coisa que o .NET faz é selecionar o compilador definido para a linguagem do código que será compilado.

É função do compilador verificar o código que está sendo analisado. Ele verificará se algum token está sendo utilizado de forma errônea e se todas as regras da linguagem estão sendo obedecidas. Basicamente ele verificará se há algum erro de digitação no código.

Compilando o código para a linguagem intermediária

Agora que o .NET começa a fazer a sua “mágica”. Ao final do processo de compilação, o compilador converte o código-fonte para uma linguagem intermediária, que no .NET, é chamada de Microsoft Intermediate Language (MSIL), ou apenas Intermediate Language (IL).

O IL é um conjunto de instruções independente da CPU, que pode ser convertido de forma mais eficiente para código nativo.

Nele temos instruções para carregar, armazenar, iniciar e chamar métodos em objetos, bem como, instruções para operações lógicas e matemáticas, controle de fluxo, acesso direto a memória, manipulação de erros, entre outros recursos.

Antes deste código ser gerado, ele precisa ser convertido em tempo de execução para um código de máquina que é entendido pela CPU do computador. Normalmente este processo é realizado por um compilador Just-in-Time (JIT). Como o CLR fornece um ou mais compiladores de acordo com a arquitetura do computador, o mesmo conjunto de instruções IL pode ser compilado e executado em qualquer arquitetura suportada pelo .NET

Quando o código intermediário é produzido, também são gerados metadados. Esses metadados descrevem os tipos utilizados no código incluindo as suas definições, as assinaturas dos seus membros, as referências a outros códigos e qualquer coisa que será utilizada em tempo de execução.

O IL e os metadados são contidos em um arquivo executável portátil (PE – portable executable), que é baseado e que estende o Microsoft PE e Common Object File Format (COFF), historicamente utilizado para conteúdo executável. Este formato de arquivo, que possui o IL ou código nativo, bem como metadados, permite que o sistema operacional reconheça características do CLR. A presença dos metadados com o código intermediário permite que o código se descreva, o que significa que não há necessidade de incluir bibliotecas de tipos ou interfaces de definição da linguagem (IDL).

Durante a execução, o sistema localiza e extrai dos metadados as informações que necessitar.

Compilando o código da linguagem intermediária para a nativa

Antes de executar o código IL, é necessário compilá-lo para a linguagem nativa, suportada pela arquitetura da máquina alvo. Para fazer este processo, o .NET fornece duas formas de conversão:

  • Um compilador Just-in-Time (JIT);
  • O Ngen.exe (Native Image Generator).

Compilação com um compilador JIT

A compilação JIT converte o código intermediário em código nativo em tempo de execução. Conforme um código IL for requisitado, o JIT o converte para código nativo.

Como o CLR fornece um compilador JIT para cada arquitetura suportada pelo .NET, um código IL pode ser compilado pelo JIT e executado em diferentes arquiteturas. No entanto, se o código chamar APIs nativas de uma plataforma, ou algum recurso específico, o código IL só poderá ser executado nesta plataforma.

A compilação JIT leva em conta a possibilidade de algum código nunca ser chamado durante a execução. Assim, em vez de usar tempo e memória para converter todo o código, em um arquivo PE nativo, ele converte apenas o que for necessário para a execução, e armazena este código nativo gerado, na memória, para ele ficar acessível para as chamadas subsequentes.

Por exemplo, na primeira vez que um método é invocado durante a execução, o JIT irá convertê-lo para código nativo e armazená-lo na memória. Quando este código for chamado novamente, o JIT irá diretamente na memória obter o código nativo, em vez de tentar convertê-lo novamente.

Compilação com o Ngen.exe

Como o compilador JIT gera o código nativo em tempo de execução, isso pode impactar um pouco na performance da aplicação. Na maioria dos casos, esta perda é aceitável ou irrisória. Além disso, o código gerado pelo compilador JIT fica vinculado ao processo que desencadeou a compilação, assim ele não pode ser compartilhado entre vários processos.

Para permitir que o código gerado possa ser compartilhado entre múltiplas chamadas ou entre vários processos que compartilham um conjunto de códigos nativos, o .NET possui o compilador Ngen.exe, também chamado de compilador Ahead-of-Time (AOT).

Este compilador gera o código nativo de uma forma muito parecida que o JIT, mas eles se diferenciam em três pontos:

  • O código IL é convertido antes do arquivo ser executado, no lugar de convertê-lo durante a execução.
  • Todo o código é convertido de uma única vez.
  • O código convertido é salvo no disco, e não na memória.

Verificação de código

Se não for desabilitado, durante a compilação para o código nativo, tanto com JIT ou com o AOT, o código IL passa por uma verificação que analisa se o código é “type safe”, o que significa que o código só acessa locais da memória que ele está autorizado a acessar.

Isso ajuda a isolar um objeto do outro e ajuda a protegê-lo contra corrupção indevida ou mal-intencionada. Ele também garante que as restrições de segurança podem ser aplicadas de forma confiável.

Para verificar se o código é “type safe”, a verificação se baseia em três afirmações (que precisam ser verdadeiras):

  • Uma referência a um tipo é estritamente compatível com o tipo a ser referenciado;
  • Somente operações apropriadamente definidas são invocadas em um objeto;
  • As identidades são de quem afirmam ser.

Se o requisito “type safe” estiver ativo e o código não passar na verificação acima, é gerando um erro de execução.

Executando o código

O processo final de execução, é a execução do código propriamente dito. Como já dito nos tópicos anteriores, durante esta execução o CLR e os metadados irão fornecer as informações que forem necessárias para a execução da aplicação.

Lendo assim, é possível notar que o processo não é simples, mas não é tão complexo, por isso que é importante o seu entendimento.

Curso de
CONHEÇA O CURSO

Introdução à análise de algoritmos

Os algoritmos fazem parte do dia a dia das pessoas. Seja uma receita culinária ou instruções para uso de medicamentos. Ou seja, é uma sequência de ações executáveis para chegar à solução de um determinado tipo de problema. Segundo Edsger Dijkstra, um algoritmo corresponde a uma descrição de um padrão de comportamento, expresso em termos de um conjunto finito de ações.

Desta forma, a análise de algoritmos (descrita e difundida por D.E. Knuth) tem como função determinar os recursos necessários para executar um dado algoritmo. Ela estuda a correção e o desempenho através da análise de correção e da análise de complexidade. Dados dois algoritmos para um mesmo problema, a análise permite decidir qual dos dois é mais eficiente.

Curso de
CONHEÇA O CURSO

Análise da correção

A análise da correção procura entender porque um dado algoritmo funciona. É preciso ter certeza de que o algoritmo está correto. Como algoritmos prometem resolver problemas, é esperado que ele cumpra essa premissa.

Análise da complexidade

A Análise da complexidade procura estimar a velocidade do algoritmo. Prever o tempo que o algoritmo vai consumir. Verificar qual é o custo de usar um dado algoritmo para resolver um problema específico. Porém, neste aspecto, encontram-se duas dificuldades:

  1. O tempo gasto depende dos dados de entrada.
  2. O tempo também depende da capacidade de hardware do computador utilizado.

Mas, mesmo assim, é possível definir a complexidade de determinado algoritmo.

Para medir o custo de execução de um algoritmo é comum definir uma função de custo ou função de complexidade, f; f(n) é a medida do tempo necessário para executar um algoritmo para um problema de tamanho n;

Para entendermos melhor, vejamos o trecho de código abaixo:

int soma = 0;
for (i=0; i<n; i++)
    soma = soma + vet[i];

O custo para execução deste algoritmo é f(n) = n. Pois o laço executa de acordo com o tamanho de n. Se n=10, logo o laço executará 10 vezes, se for igual a 100 executará 100 vezes.

Podemos tornar este algoritmo mais eficiente fazendo a seguinte modificação:

int soma = vet[0]; 
for (i=1; i<n; i++)
    soma = soma + vet[i];

Assim, temos uma nova função de complexidade, f(n) = n - 1.

Os dois trechos de códigos resolvem o mesmo problema, somar os elementos de um vetor. Porém, cada um tem um custo de execução diferente do outro, a partir da análise, podemos definir qual é o algoritmo mais eficiente para resolver este problema específico.

Melhor caso, Pior caso, Caso Médio.

Na análise da complexidade, definimos para um algoritmo, o melhor, o pior e o caso médio, como forma de mensurar o custo do algoritmo de resolver determinado problema diante de diferentes entradas.

  • Melhor caso: é quando o algoritmo representa o menor tempo de execução sobre todas as entradas de tamanho n;

  • Pior caso: maior tempo de execução sobre todas as entradas de tamanho n;

  • Caso médio (ou caso esperado): é a média dos tempos de execução de todas as entradas de tamanho n.

Desta forma, considere o problema de acessar um determinado registro de um vetor de inteiro; cada registro contém um índice único que é utilizado para acessar o elemento do vetor. O problema: dada uma chave qualquer, localize o registro que contenha esta chave; O algoritmo de pesquisa mais simples é o que faz a pesquisa sequencial.

Seja f uma função de complexidade tal que f(n) é o número de registros consultados no arquivo, temos:

  • Melhor caso: f(n) = 1 (registro procurado é o primeiro consultado);

  • Pior caso: f(n) = n (registro procurado é o último consultado ou não está presente no arquivo);

  • Caso médio: f(n) = (n+1)/2 (a média aritmética entre o melhor e o pior caso);

Além disso, a análise de algoritmos estuda certos paradigmas como divisão e conquista, programação dinâmica, gula, busca local, aproximação, entre outros que se mostraram úteis na criação de algoritmos para vários problemas computacionais.

Conclusão

Neste artigo, vimos uma breve introdução do que se trata a análise de algoritmos. Como ela é útil para definir o algoritmo mais eficiente em determinados problemas. Assim, o objetivo final não é apenas fazer códigos que funcionem, mas que sejam também eficientes.

“Um bom algoritmo, mesmo rodando em uma máquina lenta, sempre acaba derrotando (para instâncias grandes do problema) um algoritmo pior rodando em uma máquina rápida. Sempre.” – S. S. Skiena, The Algorithm Design Manual

Um abraço e até a próxima!

Curso de
CONHEÇA O CURSO

Os caminhos das certificações Microsoft (parte 1)

Olá pessoal, tudo certinho?

Muitos alunos entram em contato conosco com dúvidas relacionadas à certificação. As dúvidas mais comuns são com relação a qual prova realizar, como é a prova e também quais as possíveis vantagens que um profissional certificado pode obter.

Neste primeiro de uma série de posts sobre certificações que faremos, vamos abordar estes aspectos sobre um dos grandes players neste segmento: a Microsoft.

Curso de
CONHEÇA O CURSO

Áreas de conhecimento

A Microsoft divide as suas certificações em grandes áreas de conhecimento. Atualmente, as áreas existentes são:

  • Servers: área de conhecimento focada na parte de infraestrutura da Microsoft, contemplando Windows Server, SharePoint e até mesmo Azure;
  • Desktop: área de conhecimento focada em dispositivos Windows e mobilidade;
  • Applications: área de conhecimento focada em aplicações como Office, Office 365 e Dynamics CRM;
  • Database: área de conhecimento focada em bancos de dados e Business Intelligence com o SQL Server;
  • Developer: área de conhecimento voltada para desenvolvimento, abordando o .NET, Visual Studio e ferramentas de CI (Continuous Integration).

Neste post, levaremos mais em consideração a área Developer.

MCP? MTA? O que são essas siglas?

Antes de qualquer coisa, precisamos entender melhor algumas siglas que são famosas para certificados Microsoft. Nós temos duas siglas basicamente: MTA e MCP. É importante entendermos o significado de cada uma destas siglas para que possamos escolher até mesmo as provas que iremos prestar.

MTA é uma sigla para Microsoft Technology Associate. Este é um nível mais “iniciante” dentro da hierarquia das certificações Microsoft, sendo recomendado mais para estudantes e pessoas que estão em sua primeira experiência como profissionais de TI.

MCP é uma sigla para Microsoft Certified Professional. Essa já é uma certificação voltada para profissionais com experiência na respectiva área de conhecimento, além de ser mais reconhecida pelo mercado de trabalho.

Nada impede um estudante de uma faculdade fazer as provas voltadas ao programa dos MCPs, assim como nada impede um profissional que já tenha experiência de participar das provas relativas ao programa dos MTAs. Porém, são situações que não são muito comuns e devem ser evitadas.

Se um estudante de tecnologia fizer uma prova voltada para MCPs, ele poderá encontrar mais dificuldades que o esperado por ainda não ter uma bagagem de experiência que lhe dê mais suporte para a execução da prova. Da mesma maneira que, se um profissional que já tenha experiência fizer uma prova do programa dos MTAs, isso talvez não agregue tanto ao seu currículo. Portanto, aqui cabe uma auto-avaliação: se você julga que já tem uma experiência de mercado legal, caia de cara no programa dos MCPs.

Caso você não tenha tanta experiência ou ainda esteja em seu curso de graduação, considere fortemente realizar primeiramente as provas do programa para MTAs.

Certificações da área de conhecimento Developer

Vamos começar falando das certificações da área Developer, voltada para desenvolvedores de software.

Programa MTA (Microsoft Technology Associate)

No caso do programa para MTAs, surge a possibilidade de se realizar 3 provas de certificação:

  • Software Development Fundamentals (98-361): é a prova ideal para quem trabalha ou quer trabalhar com desenvolvimento de software, além de desejar seguir o programa MTA. Esta prova aborda questões básicas de desenvolvimento de software, como estruturas de decisão e de repetição e utilização de variáveis. Também são abordados o paradigma orientado a objetos, conceitos básicos de HTML, CSS e JavaScript e ainda conceitos de bancos de dados. Como pode ver, é de fato uma prova bem abrangente;

  • HTML 5 App Development Fundamentals (98-375): é uma prova específica para as principais tecnologias web (HTML 5, CSS 3 e JavaScript). Ela aborda conceitos básicos como utilização dos recursos do HTML 5 (canvas, tags específicas, semântica), utilização dos recursos do CSS 3 (CSSON, conceitos de layout, transições e transformações) e utilização de recursos do JavaScript (DOM, manipulação do DOM, jQuery, geolocalização, web sockets e até mesmo armazenamento com cache e local storage);

  • Software Testing Fundamentals (98-379): é uma prova específica para a área de teste de software. Essa prova está atualmente indisponível.

Veja que as provas do programa MTA, mesmo sendo em tese introdutórias, são muito abrangentes e extensas. Portanto, não se deixe enganar se for fazer uma prova do programa MTA! 😉

Para você ter o título de MTA, você não precisa realizar necessariamente as duas provas. A partir do momento que você conclui qualquer uma delas, você já passa a ser considerado um profissional MTA pela Microsoft. Porém, você ganha o título de MTA atrelado ao conteúdo da prova. Por exemplo, se você fizer o exame 98-361, você vira um profissional certificado Microsoft MTA: Software Development Fundamentals.

Caminhos - Microsoft MTA

Programa MCP (Microsoft Certified Professional)

O programa MCP já é um pouquinho mais complexo. Ele é dividido em duas vertentes: MCSA (Microsoft Certified Solutions Associate) e MCSD (Microsoft Certified Solutions Developer), sendo que a certificação MCSA está um nível abaixo da MCSD.

Para ambas as vertentes, você precisará completar um grupo de provas pré-determinado para que consiga obter o título correspondente. Agora, um ponto interessante: apesar de existirem os grupos de provas para que se consiga obter um determinado título, você não precisa realizar as provas em uma ordem pré-determinada. Para a Microsoft, a ordem em que você realizará as provas não é um fator importante, embora alguns grupos de provas sugiram uma sequência mais lógica.

Existe um outro ponto importante: a partir do momento em que você é aprovado na primeira prova do programa MCP, você já passa a ser reconhecido como um profissional MCP pela Microsoft. Agora, se quiser atingir os níveis acima do MCP, você precisará cumprir as provas previstas nos programas MCSA e MCSD.

O nível MCSA para desenvolvedores ainda pode se dividir em duas sub-trilhas: Web Applications e Universal Windows Platform. A primeira sub-trilha é focada em tecnologias Web, principalmente HTML5, CSS3, JavaScript e ASP.NET. Já a segunda sub-trilha é focada na plataforma universal do Windows e no desenvolvimento de aplicativos desktop com HTML5, CSS3 e JavaScript.

No caso da sub-trilha MCSA Web Applications, você precisará cumprir duas provas:

  • Programming in HTML5 with JavaScript and CSS3 (70-480): prova focada exclusivamente na base para qualquer aplicação web moderna: o HTML 5, o CSS 3 e o JavaScript. Os assuntos abordados na prova são muito similares à prova 98-375, porém, de uma maneira muito mais aprofundada tecnicamente falando;

  • Developing ASP.NET MVC Web Applications (70-486): é a prova da Microsoft para abordar o ASP.NET MVC 5. Apesar de o nome não deixar claro, uma dica: cai muita coisa sobre o Azure nesta prova também.

No caso da sub-trilha MCSA Universal Windows Platform, você também precisa cumprir duas provas:

  • Programming in C# (70-483): prova que aborda aspectos técnicos do C#. Nesta prova já caem conteúdos um pouco mais enroscados, como threading, tasks, reflection, aspectos avançados de orientação a objetos no C#, o namespace System.IO e até mesmo o LINQ;

  • Developing Mobile Apps (70-357): é uma prova recém-lançada pela Microsoft. Apesar do nome, ela na verdade foca na nova plataforma de desenvolvimento universal da Microsoft (Universal Windows Platform – UWP), o que engloba o próprio Windows, Windows Phone, Xbox e Surface.

Após você concluir alguma das subtrilhas MCSA, você estará apto a alcançar o título de MCSD.

Com relação ao MCSD, novamente nós temos mais seis sub-trilhas:

  • MCSD App Builder: é um título dado pela Microsoft quando esta reconhece que o profissional é capaz de criar aplicações web e/ou mobiles utilizando tecnologias modernas. Para atingir este nível, você precisará fazer ao menos uma das provas listadas abaixo. Ainda há um ponto importante a se ressaltar: você só pode obter o título de MCSD App Builder após obter o título de MCSA.
  1. Developing Microsoft Azure Solutions (70-532);
  2. Developing Microsoft Azure and Web Services (70-487);
  3. Developing Microsoft SharePoint Server 2013 Core Solutions (70-488);
  4. Developing Microsoft SharePoint Server 2013 Advanced Solutions (70-489);
  5. Universal Windows Platform – App Architecture and UX/UI (70-354);
  6. Universal Windows Platform – App Data, Services, and Coding Patterns (70-355);
  7. Administering Microsoft Visual Studio Team Foundation Server (70-496);
  8. Software Testing with Visual Studio (70-497);
  9. Delivering Continuous Value with Visual Studio Application Lifecycle Management (70-498).
  • MCSD Web Applications: o desenvolvedor que atinge esta certificação tem atestado pela Microsoft de que é capaz de desenvolver aplicações web com tecnologias modernas baseando-se no ASP.NET e no Azure. Esta é, naturalmente, a extensão da certificação MCSA Web Applications. Considerando que você já tenha o nível MCSA Web Applications, a única prova que você precisará prestar neste caso é a Developing Microsoft Azure and Web Services (70-487). Agora, um ponto importante: você não precisa necessariamente ter o nível MCSA para atingir este nível MCSD, como ocorre com a certificação MCSD App Builder. Isso quer dizer que você, além da prova 70-487, pode realizar também as provas Programming in HTML 5 with JavaScript and CSS 3 (70-480) e Developing ASP.NET MVC Web Applications (70-486). Se você passar por todas elas, você já é considerado um MCSD Web Applications e também um MCSA Web Applications, já que o MCSA exige as provas 70-480 e 70-486; provas também exigidas pelo MCSD com o acréscimo da prova 70-487. Existe mais um ponto legal para notarmos: quando você consegue o título de MCSD Web Applications, você também se torna automaticamente um MCSD App Builder. Isso ocorre porque, para você se tornar um MCSD Web Applications, você precisa fazer a prova 70-487, prova esta que conta também na lista de provas necessárias para a certificação MCSD App Builder;

  • MCSD SharePoint Applications: quando o desenvolvedor atinge esta certificação, a Microsoft atesta que o desenvolvedor é capaz de desenvolver aplicações e customizações dentro do SharePoint. Para você atingir esta certificação, você precisará cumprir as seguintes provas:

  1. Programming in HTML5 with JavaScript and CSS3 (70-480);
  2. Developing ASP.NET MVC Web Applications (70-486);
  3. Developing Microsoft SharePoint Server 2013 Core Solutions (70-488);
  4. Developing Microsoft SharePoint Server 2013 Advanced Solutions (70-489).
  • MCSD Application Lifecycle Management: esta linha de certificações é voltada para profissionais que lidam com o gerenciamento do ciclo de vida de aplicações. Esta trilha de certificações contempla as seguintes provas:
  1. Administering Microsoft Visual Studio Team Foundation Server (70-496);
  2. Software Testing with Visual Studio (70-497);
  3. Delivering Continuous Value with Visual Studio Application Lifecycle Management (70-498).
  • MCSD Azure Solutions Architect: linha de provas destinada a profissionais que utilizam a plataforma de nuvem Azure. Ela contempla as seguintes provas:
  1. Developing Microsoft Azure Solutions (70-532);
  2. Implementing Microsoft Azure Infrastructure Solutions (70-533);
  3. Architecting Microsoft Azure Solutions (70-534);
  • MCSD Universal Windows Platform: Este conjunto de provas visa atestar as habilidades do desenvolvedor com relação aos Windows Universal Apps. Esta trilha é composta pelas seguintes provas:
  1. Programming in C# (70-483);
  2. Universal Windows Platform – App Architecture and UX/UI (70-354);
  3. Universal Windows Platform – App Data, Services, and Coding Patterns (70-355).

O programa para MCPs com suas trilhas pode ser representado pela ilustração abaixo:

Caminhos - Microsoft MCP

Como você pode perceber, as trilhas contidas dentro do programa do MCP são um pouco mais complicadas de serem compreendidas no começo, já que existem muitas sub-trilhas. Porém, o grande ponto que você deve levar em conta ao escolher seguir uma determinada trilha é: “em que área quero me certificar?”. Quando você tiver de maneira clara e fixa este ponto em sua mente, a escolha de qual trilha ficará muito mais fácil. Por exemplo: se você quer se especializar no Azure, faz muito mais sentido você conseguir o título de MCP começando pela prova 70-532 e logo depois fazer as provas 70-533 e 70-534.

Por hora, é isso, rs. Temos muita coisa ainda para falar sobre as trilhas de certificações Microsoft. Nos próximos posts desta série, falaremos sobre a trilha da área de conhecimento Database. Também falaremos sobre preços para realização das provas, procedimentos de marcação para a realização das provas e também sobre como funcionam as provas de certificação da Microsoft. Se você tiver quaisquer dúvidas, não deixe de comentar aqui em baixo deste artigo! 😉

Até a próxima! o/

Curso de
CONHEÇA O CURSO

Ponteiros em C, uma abordagem básica e inicial

Nesse artigo vou falar um pouco sobre o terror dos estudantes de programação, a verdadeira bruxa que come criancinhas em noite de lua cheia … Vou falar de ponteiros.

Mas calma, não se assuste! Vamos ver que esse assunto não é nenhum bicho de sete cabeças.

Pense em ponteiros como sendo aquele colega de trabalho “sacana” que não sabe nada a não ser apontar para você quando alguma pergunta é feita para ele. Quando seu chefe pergunta qualquer coisa o seu colega aponta para você responder, afinal é você quem têm na memória as informações.

Com ponteiros ocorre algo bem parecido, vamos ver uma explicação um pouco mais técnica sobre esse assunto.

Podemos dizer que ponteiros ou apontadores (também podemos nos referir a eles desta forma), são variáveis que armazenam endereços de memória.

Mas claro, não é qualquer endereço de memória, nossos ponteiros armazenam endereços de outras variáveis.

Então, veja que aquilo que chamamos de “apontar” na realidade é simplesmente a denominação que damos a um ponteiro que contém o endereço de uma variável qualquer (e de qualquer tipo).

Agora você deve estar se perguntando:

Por que devo aprender isso, qual é o grande benefício?

É simples: ponteiros são muito úteis quando temos uma situação em que uma variável precisa ser acessada em diferentes partes do programa.

Em um caso como esse o código pode ter vários ponteiros em diversas partes do programa apontando para uma variável específica.

E o melhor de tudo é que se o dado que estiver no local de memória apontado sofrer alguma alteração, não vai ter problema, pois os ponteiro espalhados no programa apontam para o endereço de memória e não exatamente para o valor.

Deu pra perceber como o uso de ponteiros ajuda o programador? Dificilmente você vai escrever um código com menos do que algumas dezenas até centenas de páginas e poder usar vários ponteiros em uma aplicação desse tipo é mais que uma mão na roda.

Curso de
CONHEÇA O CURSO

Afinal como declaro um ponteiro?

Depois de ver como um ponteiro pode melhorar sua qualidade de vida, você deve estar se perguntando como declarar uma maravilha dessa em seus códigos.

É simples. A sintaxe de um ponteiro é a seguinte:

tipo * nome_Ponteiro;

No exemplo acima temos o tipo que é o tipo de dado da variável que vamos apontar, podendo ser int, float ou até mesmo uma struct.

Depois temos o * (asterisco) que nesse caso determina que a variável é um ponteiro. E por fim temos “Nome_Ponteiro” que, como o próprio nome diz, é o nome do ponteiro.

Seguindo esses passos teremos a declaração de um ponteiro como o apresentado abaixo:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int * ptr;

    return EXIT_SUCCESS;
}

Mas claro, isso não é o suficiente para que possamos usar um ponteiro, a única coisa que fizemos foi declarar um ponteiro e nada mais.

Agora precisamos atribuir a ele um endereço de memória de uma variável qualquer do tipo int. Para fazer isso é necessário que criemos essa variável. Por exemplo:

int valor = 10;

Depois disso teremos o endereço de memória que será atribuído a nosso ponteiro, mas é claro essa atribuição não é simples. Ela precisa ser diferenciada e isso é feito usando o & (E comercial), com esse caractere conseguimos atribuir o endereço de memória de uma variável a um ponteiro.

Veja a sintaxe:

ponteiro = &valor;

Bem simples não é mesmo? Então vamos misturar tudo isso em um código para ver no que vai dar.

#include <stdio.h>
#include <stdlib.h>

int main(void) {

    int * ptr;
    int valor = 10;

    ptr = &valor;

    printf("Endereço = %x", &valor);
    printf("Endereço = %x", ptr);
    printf("Valor = %d", *ptr);

    return EXIT_SUCCESS;
}

Exemplo de um possível output para essa execução:

Endereço = 5015936c 
Endereço = 5015936c 
Valor = 10 

Veja que no código acima temos a estrutura de um código em linguagem C e nele criamos uma variável do tipo int chamada valor a quem atribuímos o valor 10.

int valor = 10;

Depois declaramos nosso ponteiro ptr e atribuímos a ele o endereço da variável valor.

int * ptr;

ptr = &valor;

Veja bem, ponteiros só aceitam endereços de memória. Não adianta tentarmos atribuir algum valor primitivo, por exemplo.

E para se obter o endereço de uma variável usamos o operador &. Foi o que fizemos.

Feito isso usamos printf() para exibir o valor do endereço da variável valor:

printf("Endereço = %x", &valor);
printf("Endereço = %x", ptr);

Se o ponteiro ptr está armazenando o endereço da variável valor, então quando imprimirmos o ponteiro ptr teremos o mesmo resultado (o mesmo endereço) que imprimir &valor (que retorna o endereço de memória da variável), não é verdade? Pois bem, foi isso que aconteceu. Viu o resultado da execução que mostramos ali em cima?

Endereço = 5015936c 
Endereço = 5015936c 
Valor = 10 

Por ultimo, exibimos o valor que existe na variável valor, tal valor que se acessado pelo ponteiro usamos a sintaxe *ptr.

printf("Valor = %d", *ptr);

Perceba que para acessar o endereço de memória é necessário duas coisas muito importantes:

  • Primeiro: dentro de printf() use %x para exibir o endereço de memória, pois o mesmo se trata de um valor hexadecimal.

  • Segundo: para acessar o endereço de memória de uma variável use & antes dela.

É possível ainda acessar o endereço de memória de um ponteiro e isso nada tem a ver com o endereço de memória da variável, para isso, assim como fizemos com a variável valor, podemos fazer:

printf("Endereço de memória do ponteiro = %x", &ptr);

Então, recapitulando, dentro de um printf() se utilizarmos:

  • ptr estaremos acessando o endereço de memória associado ao ponteiro. Ou seja, o endereço de memória de uma variável.

  • &ptr aí já estaremos acessando o endereço de memória do ponteiro.

Para acessar o conteúdo daquele endereço associado ao ponteiro é necessário mudar um pouco a abordagem.

  • Primeiro: dentro de printf() use %d para que seja possível mostrar um inteiro.
  • Segundo: use o operador * (que nesse caso nada tem a ver com multiplicação, tudo bem?) antes do ponteiro para acessar seu valor: *ptr.

Altere o exemplo para:

printf("Endereço = %x", &valor);
printf("Endereço = %x", ptr);

printf("Valor = %d", *ptr);
printf("Valor = %d", valor);

Reiterando:

  • *ptr – A variável ptr tem o endereço da variável valor, não é? É meio caminho andado para encontrar o valor dela, não acha? E para encontrar esse valor usamos o operador * antes do nome do ponteiro.
  • valor – Estamos explicitamente imprimindo o conteúdo dessa variável valor do tipo inteiro.

É isso aí pessoal! simples, não é mesmo? Mas tudo que parece ser simples pode ser complicado.

Imagine que você queira se aventurar um pouco mais e sair da mesmice de ponteiros simples, se você esta nessa fase da vida, continue lendo …

Ponteiro para ponteiros

Opa. Quem diria, você por aqui? Bom, se chegou até aqui é porque quer se aventurar, né? To sabendo.

Então vamos lá! Para entender ponteiros para ponteiros precisamos de uma situação da vida real.

Imagine que você foi para uma balada e encontrou uma pessoa legal, vocês conversaram e essa pessoa escreveu em um papel velho seu número de telefone. Você pegou o papel, mas como ele está sujo e meio engordurado você resolve pegar um papel limpo e anotar o telefone novamente (e você está sem celular!).

Esse processo pode ser identificado em programação como um ponteiro para ponteiro. O também chamado ponteiro do ponteiro é aquele ponteiro que aponta para outro ponteiro.

Doeu minha cabeça! Vamos ver a sintaxe para ficar mais fácil de entender?

int *ptr;
int **pptr;

Veja que acima declaramos um ponteiro comum com apenas um *asterisco e depois declaramos o ponteiro do ponteiro, que nesse caso, utiliza-se dois ** asteriscos.

Essa é a declaração, já a atribuição é a seguinte:

ptr = &valor;
pptr = &ptr;

Bem simples, enquanto o ponteiro simples aponta para uma variável, o ponteiro do ponteiro aponta para o ponteiro simples.

Vamos misturar tudo isso e ver no que dá:

#include <stdio.h>
#include <stdlib.h>

int main(void) {

    int * ptr;
    int ** pptr;

    int valor = 10;

    ptr = &valor;
    pptr = &ptr;

    printf("Endereço de ptr = %x", &ptr);
    printf("Endereço de pptr = %x", &pptr);

    printf("Valor ptr = %d", *ptr);
    printf("Valor pptr = %d", **pptr);

    return EXIT_SUCCESS;
}

Veja que no código acima declaramos os ponteiros da forma que já foi explicado e suas atribuições também foram feitas.

E no final exibimos os endereços e valores dos ponteiros. Veja que apesar de os ponteiros terem endereços diferentes, o valor apontado é o mesmo.

Muito legal, não é mesmo? Finalizo por aqui. Nos vemos nos próximos artigos.

Curso de
CONHEÇA O CURSO

Operações CRUD no ASP.NET MVC 5 com o ADO.NET

Conversando com um amigo ele contou como funciona a estrutura da aplicação ASP.NET MVC da empresa onde ele recentemente começou a trabalhar.

A minha surpresa foi saber que durante a migração da aplicação desktop para ASP.NET MVC eles optaram por continuar utilizando a mesma estrutura de acesso à base de dados com o clássico e bom ADO.NET.

Não pretendo discorrer nesse artigo se essa foi uma boa ou má escolha ou se seria melhor modificar todo o acesso à base de dados durante a migração e optar por outro framework ORM.

O que essa situação me mostrou é que mesmo com
várias opções, ainda há quem prefira utilizar o ADO.NET e não há muitos artigos abordando o uso deste framework com o ASP.NET MVC.

Sendo assim, mostrarei aqui como isso pode ser feito. Então, mãos à massa!

Curso de
CONHEÇA O CURSO

Criando uma aplicação ASP.NET MVC

Para esse artigo estou utilizando o Visual Studio Community 2015. Nele, siga os procedimentos abaixo para criar o projeto.

  1. Com o Visual Studio aberto, em “File”, clique em “New > Project”, selecione o template “ASP.NET Web Application”, por fim, clique em OK.

A tela abaixo será apresentada:

  1. Como mostra a imagem, selecione a opção de template Empty e adicione a referência ao MVC. Depois clique em OK para criar o projeto:

Pronto! O nosso projeto está criado.

Criando classes do domínio

Aqui não vou me ater muito a padrões de projetos, mas para ficar próximo a uma estrutura comum quando se faz uso de um framework ORM, vamos criar no projeto classes de domínio e um repositório.

A classe de domínio será idêntica com uma classe de domínio de um framework ORM:

public class Pessoa
{
    public int Id { get; set; }

    [Required(ErrorMessage = "O campo nome é obrigatório.")]
    public string Nome { get; set; }

    public string Email { get; set; }

    public string Cidade { get; set; }

    public string Endereco { get; set; }
}

A anotação Required definida acima será utilizada para a validação na View.

Como a aplicação está utilizando o ADO.NET ela não poderá fazer uso das migrations do Entity (ou qualquer outro recurso equivalente em outro framework ORM), então, as classes de domínio devem representar exatamente a estrutura das tabelas do banco.

Ou seja, quando se utiliza o ADO.NET, deve ser empregado o princípio Database-First.

Criando o repositório

A criação do repositório é a parte parte mais crítica de uma aplicação que faz uso do ADO.NET, pois é nesta fase que as configurações de acesso são definidas.

Recomenda-se definir uma uma interface, mas neste exemplo, utilizarei uma classe abstrata:

public abstract class AbstractRepository<TEntity, TKey>
    where TEntity : class
{
    protected string StringConnection { get; } = WebConfigurationManager.ConnectionStrings["DatabaseCrud"].ConnectionString;

    public abstract List<TEntity> GetAll();
    public abstract TEntity GetById(TKey id);
    public abstract void Save(TEntity entity);
    public abstract void Update(TEntity entity);
    public abstract void Delete(TEntity entity);
    public abstract void DeleteById(TKey id);
}

Nesta classe, além dos métodos do repositório, é definido um atributo readonly (somente leitura), que obterá do arquivo web.config a string de conexão com o banco.

Nele é necessário definir essa string:

<connectionStrings>
  <add name="DatabaseCrud" connectionString="Data Source=(localdb)MSSQLLocalDB; Initial Catalog=DatabaseCrud-20161026144350; Integrated Security=True; MultipleActiveResultSets=True;"
    providerName="System.Data.SqlClient" />
</connectionStrings>

Agora implementaremos o repositório da nossa entidade:

public class PessoaRepository : AbstractRepository<Pessoa, int>
{
    ///<summary>Exclui uma pessoa pela entidade
    ///<param name="entity">Referência de Pessoa que será excluída.</param>
    ///</summary>
    public override void Delete(Pessoa entity)
    {
        using (var conn = new SqlConnection(StringConnection))
        {
            string sql = "DELETE Pessoa Where Id=@Id";
            SqlCommand cmd = new SqlCommand(sql, conn);
            cmd.Parameters.AddWithValue("@Id", entity.Id);
            try
            {
                conn.Open();
                cmd.ExecuteNonQuery();
            }
            catch (Exception e)
            {
                throw e;
            }
        }
    }

    ///<summary>Exclui uma pessoa pelo ID
    ///<param name="id">Id do registro que será excluído.</param>
    ///</summary>
    public override void DeleteById(int id)
    {
        using (var conn = new SqlConnection(StringConnection))
        {
            string sql = "DELETE Pessoa Where Id=@Id";
            SqlCommand cmd = new SqlCommand(sql, conn);
            cmd.Parameters.AddWithValue("@Id", id);
            try
            {
                conn.Open();
                cmd.ExecuteNonQuery();
            }
            catch (Exception e)
            {
                throw e;
            }
        }
    }

    ///<summary>Obtém todas as pessoas
    ///<returns>Retorna as pessoas cadastradas.</returns>
    ///</summary>
    public override List<Pessoa> GetAll()
    {
        string sql = "Select Id, Nome, Email, Cidade, Endereco FROM Pessoa ORDER BY Nome";
        using (var conn = new SqlConnection(StringConnection))
        {
            var cmd = new SqlCommand(sql, conn);
            List<Pessoa> list = new List<Pessoa>();
            Pessoa p = null;
            try
            {
                conn.Open();
                using (var reader = cmd.ExecuteReader(CommandBehavior.CloseConnection))
                {
                    while (reader.Read())
                    {
                        p = new Pessoa();
                        p.Id = (int)reader["Id"];
                        p.Nome = reader["Nome"].ToString();
                        p.Email = reader["Email"].ToString();
                        p.Cidade = reader["Cidade"].ToString();
                        p.Endereco = reader["Endereco"].ToString();
                        list.Add(p);
                    }
                }
            }
            catch(Exception e)
            {
                throw e;
            }
            return list;
        }
    }

    ///<summary>Obtém uma pessoa pelo ID
    ///<param name="id">Id do registro que obtido.</param>
    ///<returns>Retorna uma referência de Pessoa do registro encontrado ou null se ele não for encontrado.</returns>
    ///</summary>
    public override Pessoa GetById(int id)
    {
        using (var conn = new SqlConnection(StringConnection))
        {
            string sql = "Select Id, Nome, Email, Cidade, Endereco FROM Pessoa WHERE Id=@Id";
            SqlCommand cmd = new SqlCommand(sql, conn);
            cmd.Parameters.AddWithValue("@Id", id);
            Pessoa p = null;
            try
            {
                conn.Open();
                using (var reader = cmd.ExecuteReader(CommandBehavior.CloseConnection))
                {
                    if (reader.HasRows)
                    {
                        if (reader.Read())
                        {
                            p = new Pessoa();
                            p.Id = (int)reader["Id"];
                            p.Nome = reader["Nome"].ToString();
                            p.Email = reader["Email"].ToString();
                            p.Cidade = reader["Cidade"].ToString();
                            p.Endereco = reader["Endereco"].ToString();
                        }
                    }
                }
            }
            catch (Exception e)
            {
                throw e;
            }
            return p;
        }
    }

    ///<summary>Salva a pessoa no banco
    ///<param name="entity">Referência de Pessoa que será salva.</param>
    ///</summary>
    public override void Save(Pessoa entity)
    {
        using (var conn = new SqlConnection(StringConnection))
        {
            string sql = "INSERT INTO Pessoa (Nome, Email, Cidade, Endereco) VALUES (@Nome, @Email, @Cidade, @Endereco)";
            SqlCommand cmd = new SqlCommand(sql, conn);
            cmd.Parameters.AddWithValue("@Nome", entity.Nome);
            cmd.Parameters.AddWithValue("@Email", entity.Email);
            cmd.Parameters.AddWithValue("@Cidade", entity.Cidade);
            cmd.Parameters.AddWithValue("@Endereco", entity.Endereco);
            try
            {
                conn.Open();
                cmd.ExecuteNonQuery();
            }
            catch (Exception e)
            {
                throw e;
            }
        }
    }

    ///<summary>Atualiza a pessoa no banco
    ///<param name="entity">Referência de Pessoa que será atualizada.</param>
    ///</summary>
    public override void Update(Pessoa entity)
    {
        using (var conn = new SqlConnection(StringConnection))
        {
            string sql = "UPDATE Pessoa SET Nome=@Nome, Email=@Email, Cidade=@Cidade, Endereco=@Endereco Where Id=@Id";
            SqlCommand cmd = new SqlCommand(sql, conn);
            cmd.Parameters.AddWithValue("@Id", entity.Id);
            cmd.Parameters.AddWithValue("@Nome", entity.Nome);
            cmd.Parameters.AddWithValue("@Email", entity.Email);
            cmd.Parameters.AddWithValue("@Cidade", entity.Cidade);
            cmd.Parameters.AddWithValue("@Endereco", entity.Endereco);
            try
            {
                conn.Open();
                cmd.ExecuteNonQuery();
            }
            catch (Exception e)
            {
                throw e;
            }
        }
    }
}

Note que em todos os métodos a conexão é aberta manualmente. Ela será fechada graças ao uso do using. E todos os métodos “lançam” as exceções geradas para o respectivo método do repositório que fora invocado.

Curso de
CONHEÇA O CURSO

Criando o Controller

Caso estivéssemos utilizando o Entity, o controller poderia ser criado já com as views, mas no nosso ambiente, teremos que criá-los separadamente. O controller será criado com as actions:

Com o uso do repositório terá o código abaixo:

public class PessoaController : Controller
{
    private PessoaRepository respository = new PessoaRepository();
    // GET: Pessoa
    public ActionResult Index()
    {
        return View(respository.GetAll());
    }

    // GET: Pessoa/Create
    public ActionResult Create()
    {
        return View();
    }

    // POST: Pessoa/Create
    [HttpPost]
    public ActionResult Create(Pessoa pessoa)
    {
        if (ModelState.IsValid)
        {
            respository.Save(pessoa);
            return RedirectToAction("Index");
        }
        else { 
            return View(pessoa);
        }
    }

    // GET: Pessoa/Edit/5
    public ActionResult Edit(int id)
    {
        var pessoa = respository.GetById(id);

        if (pessoa == null)
        {
            return HttpNotFound();
        }

        return View(pessoa);
    }

    // POST: Pessoa/Edit/5
    [HttpPost]
    public ActionResult Edit(Pessoa pessoa)
    {
        if (ModelState.IsValid)
        {
            respository.Update(pessoa);
            return RedirectToAction("Index");
        }
        else
        {
            return View(pessoa);
        }
    }

    // POST: Pessoa/Delete/5
    [HttpPost]
    public ActionResult Delete(int id)
    {
        respository.DeleteById(id);
        return Json(respository.GetAll());      
    }
}

Pronto, agora só é necessário definir as Views.

Criando as views

O código das views é bem simples, pois podemos utilizar o scaffolding do Visual Studio.

No controller clique com o botão direito sobre a
action Index, e selecione Add View. A tela abaixo será apresentada:

Configure os dados conforme a imagem acima. Agora, modifique o código gerado para este:

@model IEnumerable<CRUDUsingMVCwithAdoNet.Models.Pessoa>
@{
    ViewBag.Title = "Index";
}
<h2>Index</h2>

<p>
    @Html.ActionLink("Adicionar Pessoa", "Create")
</p>
<table class="table" id="tblPessoas">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Nome)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Email)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Cidade)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Nome)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Email)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Cidade)
                </td>
                <td>
                    @Html.ActionLink("Editar", "Edit", new { id = item.Id }) |
                    <button type="button" class="btn btn-link" data-item="@item.Id">Deletar</button>
                </td>
            </tr>
        }
    </tbody>
</table>
@section Scripts {
<script type="text/javascript">
        $(document).ready(function () {
            $(".btn-link").click(function () {
                var id = $(this).attr('data-item');
                if (confirm("Você tem certeza que gostaria de excluir este registro?")) {
                    $.ajax({
                        method: "POST",
                        url: "/Pessoa/Delete/" + id,
                        success: function (data) {
                            $("#tblPessoas tbody > tr").remove();
                            $.each(data, function (i, pessoa) {
                                $("#tblPessoas tbody").append(
                                    "<tr>" +
                                    "   <td>" + pessoa.Nome + "</td>" +
                                    "   <td>" + pessoa.Email + "</td>" +
                                    "   <td>" + pessoa.Cidade + "</td>" +
                                    "   <td>" +
                                    "       <a href='/Pessoa/Edit/" + pessoa.Id + "'>Editar</a> |" +
                                    "       <button type="button" class="btn btn-link" data-item="" + pessoa.Id + "">Deletar</button>" +
                                    "   </td>" +
                                    "</tr>"
                                );
                            });
                        },
                        error: function (data) {
                            alert("Houve um erro na pesquisa.");
                        }
                    });
                }
            });
        });
</script>
}

Como mostra o código acima, para não ser necessário criar uma view de exclusão, esta funcionalidade já é definida na listagem.

Repita o mesmo procedimento para termos a view Create:

@model CRUDUsingMVCwithAdoNet.Models.Pessoa
@{
    ViewBag.Title = "Cadastrar";
}

<h2>Cadastrar nova Pessoa</h2>


@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>Pessoa</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.Nome, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Nome, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Nome, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Email, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Email, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Email, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Cidade, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Cidade, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Cidade, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Endereco, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Endereco, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Endereco, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Cadastrar" class="btn btn-default" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>
<script src ="~/Scripts/jquery.validate.min.js" />
<script src ="~/Scripts/jquery.validate.unobtrusive.min.js" />

E agora a view Edit:

@model CRUDUsingMVCwithAdoNet.Models.Pessoa
@{
    ViewBag.Title = "Editar";
}

<h2>Editar</h2>


@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>Pessoa</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        @Html.HiddenFor(model => model.Id)

        <div class="form-group">
            @Html.LabelFor(model => model.Nome, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Nome, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Nome, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Email, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Email, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Email, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Cidade, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Cidade, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Cidade, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Endereco, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Endereco, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Endereco, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Salvar" class="btn btn-default" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("Voltar a listagem", "Index")
</div>
<script src="~/Scripts/jquery.validate.min.js" />
<script src="~/Scripts/jquery.validate.unobtrusive.min.js" />

Como criamos o projeto a partir de um template vazio, não se esqueça de adicionar os scripts do jQuery Validation (https://jqueryvalidation.org/) e do jQuery Validation Unobtrusive (https://github.com/aspnet/jquery-validation-unobtrusive).

Pronto! A aplicação está pronta. Agora é só testar.

Executando a aplicação

Ao executar a aplicação a tela abaixo será mostrada:

Clique em Adicionar Pessoa para inserir um novo registro.

Se no cadastro nada for informado:

A validação funcionará. Se dados forem informados, o registro será salvo no banco e listado:

Caso clique em Editar, a edição estará funcionando perfeitamente:

E no caso da exclusão é exibida uma caixa de diálogo:

Que ao ser confirmada o registro será excluído:

Conclusão

Os frameworks ORM trazem produtividade para o desenvolvimento, mas o seu uso não pode nos cegar em relação ao legado. Caso seja necessário, é possível fazer uso do ADO.NET no ASP.NET MVC, sem estresse. Esta substituição só será sentida na produtividade devido a não possibilidade de uso de alguns recursos do Visual Studio.

Curso de
CONHEÇA O CURSO

Gerenciamento de memória no C#: stack, heap, value-types e reference-types

Opa pessoal, tudo certinho?

Neste post, eu vou abordar uma dúvida que aparece com uma certa frequência em nosso suporte: o que são, afinal de contas, as benditas memórias stack e heap? O que são afinal de contas os value-types e os reference-types?

Para entendermos melhor estes conceitos, precisamos também verificar um pouco sobre a criação de objetos e de variáveis primitivas, assim como os conceitos de value-types e reference-types… Então, vamos lá! o/

O compilador, de maneira geral, divide a memória em duas grandes áreas: a stack (uma área bem menor) e a heap (uma área bem maior). Seria algo simiar à ilustração abaixo:

Áreas de memória - compilador .NET

Na configuração padrão do .NET Framework, para que você tenha uma idéia melhor de como a stack é muito menor que a heap, o tamanho padrão para a memória stack é de apenas 1MB!

Ambas trabalham como pilhas, porém, a maneira como cada uma provê acesso a seu conteúdo é diferente. A stack é bem mais eficiente para localizar as coisas em seu interior com relação a heap, mesmo porque ela é bem menor.

As variáveis de alguns tipos de dados leves (tipos primitivos – int, double, bool etc. – e structs) são armazenadas diretamente na stack, a área menor e mais eficiente para localização dos conteúdos. Elas ficam diretamente nessa área justamente por serem tipos de dados que não ocupam tanto espaço na memória. O mais interessante é que o valor que elas contêm também fica junto com elas na stack. Ou seja, quando você faz a declaração abaixo:

int numero = 3;

O compilador armazena essa variável diretamente na memória stack, como na ilustração abaixo:

Alocação de memória - Value-Type

Curso de
CONHEÇA O CURSO

Perceba que o valor da variável fica junto com a própria variável. Variáveis onde isso acontece são chamadas de Value-Types, justamente porque o valor delas fica junto com a própria variável na memória stack. Assim, quando você tem o seguinte código:

if (numero >= 3)
{
    //...
}

O compilador tem acesso direto ao conteúdo, pois ele está juntinho com a própria variável na memória stack:

Acesso à memória: value-type

Agora, outros tipos de dados ocupam muito mais espaço de memória do que estes tipos leves que são value-types. Por isso, eles não podem ser armazenados diretamente na stack (caso fossem, rapidamente a memória stack seria “estourada”, causando o famoso erro StackOverflowException). Sendo assim, estes dados são armazenados na memória heap.

Vamos imaginar que você tenha o seguinte código:

class Pessoa
{
    public int Id {get; set;}
    public string Nome {get; set;}
}

Quando você cria um objeto dessa classe, este objeto será armazenado na memória heap:

Pessoa minhaPessoa = new Pessoa();

Alocação de memória: reference-type

Porém, o compilador não acessa a heap diretamente. Por que ele não acessa? Justamente porque ela é muito grande… Se ele fosse procurar o objeto minhaPessoa dentro da heap, ele iria demorar um tantinho bom de tempo. O compilador precisaria ter um jeito de acessar pela stack (que é rápida pra encontrar as coisas até mesmo por ser bem menor) o que está alocado na heap (que é bem maior). Como o compilador contorna isso? Criando uma referência dentro da stack para o objeto minhaPessoa, apontando onde na memória heap que este objeto está de fato guardado!

Processo de referência stack-heap

Essa porção de memória que é alocada na stack para apontar para uma posição de memória da heap é chamada de ponteiro. Por isso ele tem esse asterisco (*) na frente do seu nome.

Repare então que é criada uma referência da stack para uma determinada posição de memória da heap, referência essa guardada por um ponteiro na stack. Esse tipo de variável (como no caso da variável minhaPessoa, do tipo Pessoa) é chamada de Reference-Type, já que é necessário uma referência da stack para a heap para que esta variável seja acessível. Variáveis reference-type geralmente precisam que seja chamado o respectivo construtor através da palavra-chave new, pois ele é que define que uma porção de memória da heap deverá ser utilizada para guardar aquele objeto.

Dessa maneira, quando temos o código abaixo:

if (minhaPessoa.Id > 2)
{
    //...
}

O compilador faz o acesso ao objeto minhaPessoa através da stack, ou seja, através do ponteiro. Esse ponteiro encaminha o compilador para a posição de memória da heap que contém de fato o objeto minhaPessoa.

Acesso à memória: reference-types

Resumindo:

  • Value-Type: são tipos leves (como os tipos primitivos e structs) que ficam armazenados diretamente na memória stack. Os valores das variáveis ficam armazenados juntamente com as próprias variáveis, sendo o acesso ao seu conteúdo feito de maneira direta;
  • Reference-Type: tipos pesados (objetos criados a partir de classes, etc.) que ficam armazenados na heap. Para não sacrificar a performance, é criada uma referência (ponteiro) na stack que aponta para qual posição de memória o objeto está armazenado na heap. O acesso é feito via essa referência na stack. Sendo assim, o acesso ao conteúdo é indireto, dependendo dessa referência;
  • Stack: porção de memória pequena onde os value-types e os ponteiros ficam;
  • Heap: porção maior de memória onde os reference-types ficam de fato alocados… Para se fazer o acesso a eles, precisamos de um ponteiro na stack que indique a posição de memória na heap onde o objeto está de fato alocado.

O detalhe interessante é que a imensa maioria das linguagens gerenciadas (como o Java, Swift e o próprio C#) seguem exatamente esta arquitetura de gerenciamento de memória… Isso quer dizer que o que vimos aqui para o C#, pode ser aplicado para outras linguagens com pequenas variações. o/

É isso aí pessoal. Obrigado por terem lido este post! =)

Nos próximos posts, continuaremos as discussões sobre os value-types e os reference-types e veremos um tipo de dado que foge um pouco da regra: as strings.

Curso de
CONHEÇA O CURSO