Estruturas de dados em  C#

As estruturas de dados são formas específicas de organizar e armazenar dados em um programa, permitindo manipulação eficiente. A escolha correta da estrutura de dados impacta diretamente no desempenho da aplicação, influenciando o tempo de execução e o uso de memória. Neste artigo, vamos explorar as principais estruturas de dados em C#, explicar como funcionam e quando utilizá-las. O que são estruturas de dados? As estruturas de dados são formas específicas de organizar e armazenar dados de maneira que possibilitem uma manipulação eficiente. Elas são essenciais para garantir que as operações sobre os dados, como inserção, remoção, e acesso, sejam feitas de forma rápida e eficiente. O conceito de estrutura de dados envolve a organização dos dados em diferentes formatos para atender a necessidades específicas, como: Eficiência no uso da memória: algumas estruturas de dados são mais compactas e otimizadas para armazenar grandes volumes de informações. Eficiência na execução de operações: dependendo do tipo de estrutura, certas operações podem ser mais rápidas, como buscar um elemento ou adicionar um novo item. Estruturas de dados lineares As estruturas de dados lineares são aquelas em que os elementos são organizados de forma sequencial, ou seja, cada elemento tem um único predecessor e um único sucessor (exceto os elementos nas extremidades). Elas são ideais quando há a necessidade de acessar ou manipular dados de forma simples e direta, respeitando a ordem de inserção. Vamos conhecer algumas das estruturas lineares mais comuns em C#: Array Um array é uma estrutura de dados fixa, onde os elementos são armazenados de maneira contígua na memória. Eles têm um tamanho definido no momento da criação, e esse tamanho não pode ser alterado após a criação. Ele se caracteriza por: Acesso rápido aos elementos, usando o índice. Tamanho fixo, o que significa que o número de elementos deve ser conhecido de antemão. A memória é alocada de forma contígua, ou seja, os elementos são armazenados em sequência, um logo após o outro, o que torna o acesso mais rápido. A criação de um array pode ser feita da seguinte forma: int[] numeros = { 1, 2, 3, 4, 5 }; Console.WriteLine(numeros[2]); O código acima, acessa o elemento que está na  posição 2 que é igual a  3. List<T> A classe “List<T>” é uma coleção dinâmica em C# que permite armazenar elementos de forma sequencial. Diferente de um array, o tamanho da lista pode ser alterado dinamicamente, o que a torna mais flexível. Suas características são: Capacidade de redimensionar-se automaticamente conforme os elementos são adicionados ou removidos. Acesso a elementos via índice, como em um array, mas com a vantagem de não precisar definir o tamanho de antemão. A sua  construção pode ser feita da seguinte forma: List numeros = new List { 1, 2, 3, 4, 5 }; numeros.Add(6); Console.WriteLine(numeros[5]); No código acima, nós criamos uma lista com 5  elementos. Após isso, adicionamos mais um elemento (6). Por fim,  apresentamos no  console o elemento que está na posição 5 da lista, que neste caso é o 6. LinkedList<T> A ”LinkedList<T>” é uma lista encadeada onde cada elemento (nó) contém uma referência ao próximo elemento. Ao contrário de arrays e listas, os elementos da LinkedList não são armazenados de forma contígua na memória. Algumas características são: Cada elemento contém um ponteiro para o próximo elemento, o que permite inserções e remoções eficientes, especialmente no início ou no meio da lista. Acesso sequencial aos elementos, não é possível acessar diretamente um elemento por índice. Agora vamos ver um exemplo de LinkedList: LinkedList numeros = new LinkedList(); numeros.AddLast(1); numeros.AddLast(2); numeros.AddFirst(0); Console.WriteLine(numeros.First.Value); No código acima, criamos uma LinkedList de inteiros e adicionamos três valores: dois no final da lista e um no início. Em seguida, utilizamos a propriedade First para acessar o primeiro valor da lista, que, neste caso, é o 0. A LinkedList permite uma navegação flexível, sendo possível adicionar ou remover elementos em qualquer posição de forma eficiente. Quando usar: útil quando você precisa realizar inserções e remoções frequentes em qualquer posição da lista, especialmente no início ou no meio, onde uma List<T> poderia ser menos eficiente. Stack<T> A Stack<T> e uma estrutura de dados do tipo LIFO (Last In, First Out). Isso significa que o último item a ser inserido é o primeiro a ser removido, ou seja, a pilha funciona como uma “pilha de pratos”, onde você adiciona um prato no topo e retira o último prato colocado. Esse comportamento é útil em situações em que você precisa processar dados de trás para frente ou manter um controle sobre operações anteriores, como em um histórico de navegação ou execução de funções recursivas. Vamos ver o código do exemplo acima: Stack pilha = new Stack(); pilha.Push(1); pilha.Push(2); Console.WriteLine(pilha.Pop()); Console.WriteLine(pilha.Pop()); Quando usamos o “Pop()”, ele remove o último elemento inserido, que, no caso, é o 2, seguido pelo 1. Queue<T> A Queue<T> é uma estrutura de dados do tipo FIFO (First In, First Out). Isso significa que o primeiro item a ser inserido é o primeiro a ser removido.  Imagine uma fila de pessoas, onde a primeira pessoa a entrar na fila é a primeira a ser atendida. Esse comportamento é útil em situações onde os dados precisam ser processados na ordem em que foram recebidos, como em sistemas de processamento de tarefas ou controle de requisições. Vamos ver o código do exemplo acima: Queue fila = new Queue(); fila.Enqueue(1); fila.Enqueue(2); Console.WriteLine(fila.Dequeue()); Console.WriteLine(fila.Dequeue()); No código acima, o “Dequeue()” remove o primeiro elemento inserido. Assim, o primeiro Dequeue remove o 1 e o segundo Dequeue remove o 2, seguindo o princípio FIFO. Se você quiser se aprofundar mais sobre o Queue, pode conferir o artigo completo sobre o assunto. Acelere a sua carreira conosco! Se você é Desenvolvedor .NET Júnior e quer acelerar sua carreira até nível Pleno com salário de R$7k+, ou mesmo busca a primeira vaga, conheça a Mentoria .NET Start: Clique aqui Se é Desenvolvedor .NET Pleno ou Sênior e quer virar referência técnica em sua equipe e mercado, com salário de R$10k+, conheça a Mentoria .NET Expert: Clique aqui Conclusão

Programação assíncrona e concorrente com Task em C#

A programação assíncrona e concorrente são conceitos fundamentais para a criação de aplicações que precisam lidar com múltiplas operações ao mesmo tempo, como quando é necessário fazer requisições de rede, realizar processamento paralelo ou executar tarefas pesadas sem bloquear a interface do usuário. Em C#, o conceito de Task é uma peça central para implementar essas funcionalidades de maneira eficiente. O que é uma Task? Uma Task é como uma tarefa que pode ser executada de forma independente, sem bloquear o restante do programa. Ela é usada para fazer tarefas em segundo plano enquanto o programa continua executando. Você pode esperar o resultado de uma Task usando o comando “await” ou controlar o que acontece depois dela com métodos como “ContinueWith” ou “WaitAll”. Usando Task, conseguimos evitar que a “thread principal” (a parte do programa que controla a interface com o usuário) fique bloqueada. Isso permite que o programa faça várias ações ao mesmo tempo de maneira mais eficiente, sem travar ou demorar para responder. Task.Run: iniciando tarefas em uma thread separada O método “Task.Run” é utilizado para iniciar uma tarefa em uma thread separada. Isso é particularmente útil quando você precisa evitar o bloqueio da thread principal da aplicação, especialmente em aplicações com interface gráfica (GUI), onde o bloqueio da thread principal pode levar a uma interface congelada. Console.WriteLine(“Iniciando tarefa…”); Task.Run(() => { // Simulando uma operação pesada System.Threading.Thread.Sleep(2000); Console.WriteLine(“Tarefa concluída.”); }); Console.WriteLine(“Aguardando a tarefa…”); Console.ReadLine(); No exemplo acima, a tarefa que simula uma operação pesada (sleep de 2 segundos) é executada em uma thread separada, permitindo que o código continue executando sem bloquear a aplicação. Task.Delay: criando atrasos assíncronos Em alguns casos, é necessário inserir um atraso na execução de uma tarefa sem bloquear a thread principal. Isso é facilmente feito com o método “Task.Delay”, que permite aguardar de forma assíncrona por um tempo especificado. Console.WriteLine(“Iniciando atraso…”); await Task.Delay(3000); Console.WriteLine(“Atraso concluído.”); Neste exemplo, a tarefa aguarda por 3 segundos sem bloquear a thread principal. O uso de “await” permite que o código continue executando enquanto aguarda o atraso. Task: tarefas com retorno de valor Quando você precisa que uma tarefa assíncrona retorne um valor, você pode usar Task<T>. Essa classe permite que você execute uma operação assíncrona que retorna um valor do tipo T, que pode ser qualquer tipo de dado como “int”, “string”, ou até mesmo objetos complexos. public static async Task Main() { Console.WriteLine(“Iniciando tarefa de busca …”); string resultado = await BuscarUsuario(“José”); Console.WriteLine($”Usuário: {resultado} encontrado.”); } public static async Task BuscarUsuario(string nome) { Console.WriteLine($”Buscando {nome} no banco…”); await Task.Delay(3000); return “José Santos”; } Neste exemplo, o método “BuscarUsuario” é uma tarefa assíncrona que retorna um valor do tipo “string”. A operação de busca é realizada após um atraso simulado de 3 segundos, e o valor retornado é o resultado da busca no “banco“. Task.WaitAll: aguardando múltiplas tarefas Quando você tem várias tarefas em execução simultaneamente e precisa aguardar que todas terminem antes de prosseguir, o método “Task.WaitAll” é ideal. Ele permite aguardar a conclusão de várias tarefas de uma vez. public static async Task Main() { Console.WriteLine(“Iniciando múltiplas tarefas…”); Task task1 = Task.Run(() => ProcessaTarefa(1)); Task task2 = Task.Run(() => ProcessaTarefa(2)); Task task3 = Task.Run(() => ProcessaTarefa(3)); await Task.WhenAll(task1, task2, task3); Console.WriteLine(“Todas as tarefas concluídas.”); } public static void ProcessaTarefa(int id) { System.Threading.Thread.Sleep(2000); Console.WriteLine($”Tarefa {id} concluída.”); } Neste exemplo, três tarefas são executadas simultaneamente. Usando “Task.WhenAll”, a execução do código só prossegue após todas as tarefas terem sido completadas. Task.ContinueWith: encadeando tarefas O método “Task.ContinueWith” permite encadear a execução de uma tarefa após a conclusão de outra. Isso é útil quando você precisa realizar uma série de operações assíncronas em sequência. Console.WriteLine(“Iniciando tarefa…”); Task.Run(() => { // Simulando tarefa inicial System.Threading.Thread.Sleep(2000); Console.WriteLine(“Tarefa 1 concluída.”); }) .ContinueWith(task => { // Tarefa a ser executada após a primeira System.Threading.Thread.Sleep(1000); Console.WriteLine(“Tarefa 2 concluída.”); }); Console.WriteLine(“Aguardando tarefas…”); Console.ReadLine(); Neste exemplo, a segunda tarefa será executada assim que a primeira for concluída, sem a necessidade de aguardar explicitamente a primeira tarefa usando “await”. Task vs async/await: quando usar cada abordagem Embora “Task” seja uma abstração importante para programação assíncrona, o “async/await” é uma maneira mais moderna e legível de lidar com operações assíncronas. A principal diferença entre os dois é que “async/await” proporciona uma sintaxe mais simples e menos propensa a erros ao escrever código assíncrono. Quando usar Task: Quando você precisa de mais controle sobre o comportamento da tarefa, como no caso de encadear tarefas ou esperar por múltiplas tarefas. Quando a operação assíncrona não precisa ser aguardada diretamente no método. Quando usar async/await: Quando você deseja uma sintaxe mais simples e legível. Quando a operação assíncrona precisa ser aguardada diretamente no fluxo do método. Em muitas situações, você pode combinar as duas abordagens. Por exemplo, você pode usar “Task.Run” para executar algo em segundo plano e, dentro de um método “async”, aguardar essa tarefa com “await”. Acelere a sua carreira conosco! Se você é Desenvolvedor .NET Júnior e quer acelerar sua carreira até nível Pleno com salário de R$7k+, ou mesmo busca a primeira vaga, conheça a Mentoria .NET Start: Clique aqui Se é Desenvolvedor .NET Pleno ou Sênior e quer virar referência técnica em sua equipe e mercado, com salário de R$10k+, conheça a Mentoria .NET Expert: Clique aqui Conclusão O uso de Task no C# permite que desenvolvedores criem aplicativos mais rápidos e responsivos ao evitar o bloqueio de threads e executar operações em segundo plano. Técnicas como “Task.Run”, “Task.Delay”, “Task.WaitAll” e “Task.ContinueWith” são ferramentas muito úteis para gerenciar tarefas assíncronas e concorrentes. Quando combinado com “async/await”, você pode escrever código assíncrono de forma clara e eficiente, garantindo uma melhor experiência para o usuário.

Índices e intervalos em arrays no C#

Os arrays são estruturas fundamentais em C#, permitindo armazenar e manipular coleções de elementos do mesmo tipo. A partir do C# 8.0, novas funcionalidades foram introduzidas para tornar o trabalho com arrays ainda mais eficiente. Neste artigo vamos explorar desde os índices tradicionais até as últimas novidades como índices negativos com o operador “^” e intervalos “(..)” para criar subarrays. Índices tradicionais em arrays Nos arrays, os índices são utilizados para acessar elementos individuais. Eles começam do zero, ou seja, o primeiro elemento está no índice 0, o segundo no índice 1, e assim por diante. Se você tentar acessar um índice fora dos limites do array (numeros[5]), uma exceção “IndexOutOfRangeException” será lançada. Índices negativos com o operador ^ Com o C# 8.0, foi introduzido o operador “^” para trabalhar com índices relativos ao final do array. Isso simplifica o acesso ao último elemento ou a elementos próximos ao fim. Aqui, o número após o operador “^” indica a posição a ser acessada de trás para frente no array. Por exemplo: int[] numeros = { 10, 20, 30, 40, 50 }; Console.WriteLine(numeros[^1]); Console.WriteLine(numeros[^2]); No exemplo acima, o console exibirá os valores 50 (último elemento) e 40 (penúltimo elemento). Intervalos com o operador .. O operador “..” permite criar subarrays (fatias ou slices) de maneira concisa. Ele utiliza dois índices, um inicial e um final, separados por “..”. Regras para usar o operador .. Ao definir um intervalo como [inicial..final], você especifica onde o intervalo começa e termina. int[] numeros = { 10, 20, 30, 40, 50 }; int[] fatia1 = numeros[1..3]; Console.WriteLine(string.Join(“, “, fatia1)); Nesse exemplo, a fatia inclui os elementos “numeros[1]” (20) e “numeros[2]” (30), mas exclui o elemento “numeros[3]”. Se você omitir o índice inicial (..final), o intervalo começa no primeiro elemento do array e vai até o índice especificado (não incluído). int[] fatia2 = numeros[..3]; Console.WriteLine(string.Join(“, “, fatia2)); O intervalo “[..3]” inclui os elementos “numeros[0]”, “numeros[1]” e “numeros[2]”. Se você omitir o índice final (inicial..), o intervalo vai do índice inicial especificado até o último elemento do array. int[] fatia3 = numeros[2..]; Console.WriteLine(string.Join(“, “, fatia3)); Nesse caso, o intervalo “[2..]” inclui todos os elementos a partir de “numeros[2]” até o final do array. Se você omitir tanto o índice inicial quanto o final (..), o intervalo abrange o array inteiro. Essa sintaxe equivale a criar uma cópia completa do array. int[] fatiaCompleta = numeros[..]; Console.WriteLine(string.Join(“, “, fatiaCompleta)); Combinação com o operador ^ O operador “^” pode ser combinado com “..” para criar intervalos relativos ao final do array. Isso é útil para trabalhar com os últimos elementos de uma coleção. int[] ultimos = numeros[^3..]; Console.WriteLine(string.Join(“, “, ultimos)); O [ ^3.. ] começa no terceiro elemento a partir do final e vai até o final do array. A saída será 30, 40, 50. int[] semOsUltimos = numeros[..^2]; Console.WriteLine(string.Join(“, “, semOsUltimos)); O [ ..^2 ] vai do início do array até dois elementos antes do final. O resultado será 10, 20, 30 Combinando intervalos com métodos de extensão Uma outra possibilidade do C# é a de combinar intervalos com métodos de extensão como “Where”, “Select”, e outros disponíveis no LINQ. Isso permite criar filtros e transformações dinâmicas diretamente sobre fatias de arrays. Você pode usar o operador “..” para criar um subarray e aplicar um filtro dinâmico com o método “Where”: int[] numeros = { 10, 15, 20, 25, 30 }; var pares = numeros[1..4].Where(n => n % 2 == 0); Console.WriteLine(string.Join(“, “, pares)); O intervalo “[1..4]” seleciona os elementos do índice 1 ao índice 3  (15, 20, 25). Após isso, o método “Where” filtra os elementos que são pares. A saída resulta em 20. Além de filtrar, você pode transformar os valores de um intervalo utilizando o método “Select”: int[] numeros = { 10, 15, 20, 25, 30 }; var dobrados = numeros[2..].Select(n => n * 2); Console.WriteLine(string.Join(“, “, dobrados)); Neste exemplo, o intervalo “[2..]” seleciona todos os elementos a partir do índice 2 (20, 25, 30). O método “Select” aplica a transformação “n * 2” a cada elemento do intervalo. O resultado será uma nova lista com os números 40, 50, 60 armazenados na variável “dobrados”. Acelere a sua carreira conosco! Se você é Desenvolvedor .NET Júnior e quer acelerar sua carreira até nível Pleno com salário de R$7k+, ou mesmo busca a primeira vaga, conheça a Mentoria .NET Start: Clique aqui Se é Desenvolvedor .NET Pleno ou Sênior e quer virar referência técnica em sua equipe e mercado, com salário de R$10k+, conheça a Mentoria .NET Expert: Clique aqui Conclusão Os índices negativos e intervalos no C# oferecem novas possibilidades para manipular arrays e coleções de forma simples e eficiente. Com essas funcionalidades, o código se torna mais expressivo, eliminando a necessidade de cálculos manuais de índices. Além disso, intervalos e índices negativos podem ser combinados com outros recursos modernos do C# para criar soluções robustas e claras. Explore essas funcionalidades em seus projetos e veja como elas podem simplificar suas implementações!