Slides de Aula - Unidade IV
Slides de Aula - Unidade IV
Slides de Aula - Unidade IV
Programação Orientada
a Objetos I
Classes e algoritmos.
Expressões lambda
Uma expressão lambda é, em sua essência, uma forma compacta de definir um método.
Ela separa os parâmetros da função de seu corpo (operador “=>”). Por exemplo, a expressão
lambda (x, y) => x + y define uma função que pega dois parâmetros e retorna sua soma.
Ao usar essa expressão, programadores podem criar funções sem precisar nomeá-las, o
que é especialmente útil em operações curtas, usadas como argumento para métodos de
ordem superior.
Um dos principais lugares em que as expressões lambda são usadas é com Language
Integrated Query (LINQ), uma série de extensões de métodos que permitem manipular
coleções de dados de maneira declarativa.
Além da sintaxe concisa, o que realmente torna as
expressões lambda poderosas é o fato de elas capturarem o
contexto em que são definidas, ou seja, podem acessar
variáveis do seu escopo circundante, permitindo a criação de
closures. Essa capacidade de capturar o estado as torna
extremamente flexíveis, permitindo aos programadores criar
funções que se comportam de acordo com o contexto em que
foram criadas.
LINQ: consultas e operadores
Fonte: autoria
própria.
LINQ: consultas e operadores
Fonte: autoria
própria.
LINQ: consultas e operadores
Podem ser compreendidos como um meio de testar a forma ou o padrão de um dado em vez
de simplesmente seu valor, oferecendo uma maneira mais expressiva de trabalhar com
dados e possibilitando verificar não só tipos, mas também valores e estruturas internas.
Na prática, significa que os desenvolvedores podem realizar ações específicas com base em
características detalhadas dos dados em vez de apenas seu tipo ou valor superficial.
Uma de suas vantagens no C# é a capacidade de simplificar o código. Antes da sua
introdução, os desenvolvedores, muitas vezes, precisavam escrever várias instruções
condicionais aninhadas para avaliar diferentes propriedades e valores de um objeto.
Com os padrões de correspondência, o processo é mais direto e menos propenso a erros.
Outra característica valiosa é a integração deles com outras
funcionalidades, como as expressões de switch e as tuplas.
É possível usá-los para identificar padrões específicos, mas
também para destrinchar dados em componentes mais
gerenciáveis, que podem ser processados ou transformados
de acordo com as necessidades do desenvolvedor.
Padrões de correspondência
Alguns padrões.
Quando vários clientes tentam retirar dinheiro da mesma conta ao mesmo tempo, o sistema
deve garantir que o saldo da conta seja atualizado corretamente para evitar que mais
dinheiro seja retirado do que o disponível.
Quando um sistema tenta realizar transferências entre duas contas em ordens diferentes
(conta A para conta B e vice-versa) e cada operação exige bloqueios exclusivos nas contas
envolvidas, pode haver um impasse se as operações forem simultâneas e cada uma esperar
a outra liberar o bloqueio.
Quando vários clientes tentam reservar o último assento disponível em um voo, o sistema
precisa garantir que apenas um cliente consiga a reserva, enquanto os outros recebem uma
mensagem informando que o assento não está mais disponível.
Durante promoções ou lançamentos de produtos populares,
muitos usuários podem tentar comprar o mesmo item ao
mesmo tempo. O sistema deve gerenciar o estoque
corretamente, garantindo que os pedidos sejam processados
com base na disponibilidade e evitando vender mais itens do
que o disponível em estoque.
Threading – desafios reais
Em ambientes de trabalho colaborativo, como Google Docs ou Microsoft Office Online, vários
usuários podem editar um documento simultaneamente. O sistema deve garantir que as
edições de um usuário não sobrescrevam indevidamente as de outro, mantendo o
documento consistente para todos.
Em jogos de mundo aberto, em que vários jogadores podem interagir com os mesmos
objetos ou NPCs (personagens não jogáveis), o servidor do jogo precisa sincronizar as ações
para garantir que todos os jogadores vejam o mesmo estado do mundo em tempo real.
Em ambientes como hospitais ou departamentos de atendimento ao cliente, em que pessoas
são atendidas com base em sua ordem de chegada ou prioridade, o sistema deve garantir
que cada pessoa ou requisição seja atendida de forma justa e ordenada.
Em todos os exemplos, é crucial que os sistemas sejam
projetados para lidar com acessos concorrentes de maneira
apropriada, garantindo a integridade dos dados, a justiça e
uma experiência de usuário eficiente. Falhar em administrar
adequadamente esses desafios pode resultar em erros, perda
de dados ou insatisfação do cliente.
Threading – detalhes técnicos
É uma unidade de trabalho que pode ser executada de forma assíncrona. A biblioteca de
tarefas paralelas, conhecida como Task Parallel Library (TPL), introduziu o conceito de tarefa
como peça fundamental para a programação assíncrona e paralela.
A programação assíncrona permite que a aplicação inicie uma operação – como a solicitação
ao servidor – e continue executando outras tarefas enquanto espera a conclusão. Quando a
operação assíncrona é concluída, a aplicação é notificada e pode processar os resultados,
tudo isso sem ter bloqueado a execução principal da aplicação.
A programação paralela se relaciona à execução simultânea de múltiplas operações. Em
sistemas modernos, com múltiplos núcleos de processador, é possível executar várias
tarefas ao mesmo tempo, literalmente em paralelo.
Por exemplo, se tivéssemos um grande conjunto de dados
a processar, poderíamos dividi-lo em partes menores e
processar várias partes simultaneamente, em diferentes
núcleos. A programação paralela permite que as aplicações
tirem proveito dessa capacidade, acelerando operações que
seriam demoradas se executadas sequencialmente.
Task vs Thread
A TLP fornece ferramentas e estruturas para facilitar tanto a programação assíncrona quanto
a paralela; com ela, os desenvolvedores podem iniciar tarefas assíncronas, aguardar sua
conclusão e paralelizar operações, como loops, para que sejam executadas
simultaneamente em múltiplos núcleos.
Tanto threads quanto tasks lidam com a execução concorrente de código, mas abordam
essa concorrência de maneiras diferentes e servem a propósitos distintos.
A classe Thread representa uma execução singular e independente no nível do sistema
operacional. Ao criar e iniciar uma nova instância de Thread, estamos literalmente solicitando
que o sistema operacional aloque recursos para uma nova thread de execução.
O desenvolvedor tem um controle granular sobre a execução,
mas, com esse controle, vem a responsabilidade de gerenciar
explicitamente todos os aspectos dela, como sincronização e
possíveis conflitos dessa thread.
Task vs Thread
A Task é uma abstração de alto nível sobre a execução assíncrona e, muitas vezes, pode ser
vista como representação de uma operação ocorrendo em segundo plano.
Uma Task não é uma thread em si; em vez disso, a TPL utiliza um pool de threads para
executar tasks.
Pool de threads é uma coleção pré-inicializada de threads prontas para executar tarefas,
sem a necessidade de criar novas threads do zero a cada vez que uma operação
concorrente é necessária.
Ao criar e iniciar uma Task, ela será agendada para execução em uma das threads desse
pool, sem a necessidade de criar explicitamente uma nova thread do sistema operacional.
Reutilizar threads de um pool é geralmente mais eficiente do
que criar e destruir constantemente novas threads.
A abstração fornecida pela Task permite uma programação
assíncrona mais fácil e intuitiva.
A TPL se encarrega de muitos detalhes de gerenciamento e
sincronização, permitindo que os desenvolvedores se
concentrem na lógica do aplicativo.
Task vs Thread
O tipo Task é a representação principal de uma unidade de trabalho que pode estar em
andamento ou já concluída.
Quando essa unidade de trabalho é projetada para retornar um valor após sua conclusão,
utilizamos Task<T>, em que T especifica o tipo do valor a ser retornado.
Dois modificadores-chave: async e await. O async permite definir métodos que retornarão
tasks; e, dentro desses métodos, a palavra-chave await chama outros métodos que retornam
tasks, pausando temporariamente a execução do método atual até que a task invocada seja
concluída, mas sem bloquear a thread em que está sendo executada.
Iniciar uma task é uma operação simples, geralmente
realizada usando o método Task.Run(), que pega um
delegado e o executa em uma thread separada.
Quando uma task é finalizada, é importante considerar o
tratamento adequado, especialmente porque ela pode lançar
exceções que, se lançadas dentro de uma tarefa, serão
retransmitidas ao tentarmos acessar o resultado da task ou
explicitamente esperar sua conclusão.
Task
Embora o APM forneça um grande controle sobre as operações assíncronas, pode se tornar
complexo rapidamente, especialmente ao encadear várias operações assíncronas.
Qual das seguintes afirmações é correta sobre Tasks, Threads, APM e EAP em C#?
Qual das seguintes afirmações é correta sobre Tasks, Threads, APM e EAP em C#?
O Quick Sort é um algoritmo de ordenação mais avançado e eficiente para conjuntos maiores
de dados (Cormen et al., 2002), baseando-se em uma técnica de divisão e conquista.
Quando bem implementado, o Quick Sort pode ser significativamente mais rápido que os
algoritmos mencionados, especialmente para listas grandes.
Aplicações práticas.
Selection Sort
Em sistemas simples de gerenciamento de bibliotecas, em que o acervo é limitado e a
frequência de novas adições e pesquisas é baixa, o Selection Sort pode ordenar livros por título
ou autor, dada a simplicidade do algoritmo e a ausência de necessidade de alto desempenho.
Quick Sort
Em um e-commerce com milhares de produtos, ao se fazer uma pesquisa, os resultados
precisam ser ordenados por relevância, preço ou avaliação do cliente rapidamente. Dado o
grande volume de produtos e a necessidade de respostas rápidas, o Quick Sort pode ser uma
excelente escolha, pois é eficiente em grandes conjuntos de dados e geralmente supera os
algoritmos de ordenação.
Pilhas e filas
Uma pilha, como o nome sugere, pode ser visualizada como uma pilha de pratos. Imagine
que você está empilhando pratos um sobre o outro. O prato que você coloca por último é o
primeiro que você tira quando começa a desempilhar – comportamento conhecido como last
in, first out (Lifo), ou seja, o último item que entra é o primeiro a sair.
Uma das aplicações mais intuitivas da pilha é o recurso “desfazer” (ou “undo”), presente em
muitos softwares, desde editores de texto até softwares de design gráfico. Quando você
executa uma série de ações – como digitar, apagar ou desenhar –, elas são empilhadas. Se
você decide desfazer sua ação mais recente, a operação no topo da pilha é revertida.
Quanto mais vezes você pressiona “desfazer”, mais ações são desempilhadas e revertidas
em ordem.
Fonte:
https://commons.wikim
edia.org/wiki/File:Torre_
de_Han%C3%B3i.jpg
Torre de Hanói – recursão
Fonte:
https://commons.
wikimedia.org/wiki
/File:Torre_de_Ha
n%C3%B3i.jpg
Fila
Pode ser comparada a uma fila comum, de pessoas esperando num banco, por exemplo.
A primeira pessoa que entra na fila é a primeira a ser atendida e a sair dela – comportamento
conhecido como first in, first out (Fifo).
Assim, numa fila temos operações principais, como enqueue – que adiciona um elemento ao
final dela – e dequeue – que remove o elemento do início.
Filas envolvem gestão de tarefas em sequência, como em sistemas de impressão, em que o
primeiro documento enviado para a impressora é o primeiro a ser impresso. Se vários
usuários enviarem documentos para uma impressora ao mesmo tempo, a impressora os
enfileira e os imprime em ordem, baseando-se no Fifo.
Quando um e-mail é enviado, ele não é transmitido
imediatamente para o destinatário; em vez disso, é colocado
numa fila de mensagens a enviar.
O servidor de e-mail processa essa fila, enviando uma
mensagem de cada vez, na ordem em que foram recebidas.
Pilhas e filas
Tanto pilhas quanto filas são facilmente implementadas, respectivamente, por meio das
classes Stack e Queue, disponíveis na biblioteca-padrão .NET.
Elas oferecem métodos e propriedades que facilitam a manipulação e o gerenciamento de
dados de acordo com as características inerentes a cada estrutura.
Pilhas e filas são mais do que meras abstrações da ciência da computação; estão
incorporadas em muitos sistemas e aplicativos que usamos no dia a dia, embora possamos
não as perceber diretamente.
Entender o funcionamento dessas estruturas é crucial para qualquer desenvolvedor, pois
fornecem mecanismos para gerenciar dados de maneira ordenada e previsível.
Dicionários e conjuntos
O tempo necessário para encontrar um item numa tabela não depende diretamente do número
de elementos armazenados na estrutura.
É importante observar que a eficiência de uma tabela depende de uma boa função hash
e da capacidade de lidar com colisões (quando duas chaves diferentes resultam no
mesmo índice).
Considere um serviço de streaming de música como o Spotify. Quando busca uma música ou
artista, o sistema precisa encontrar essa informação rapidamente em meio a milhões de
músicas e artistas disponíveis. A busca eficiente em grandes volumes de dados pode ser
viabilizada por dicionários, em que a chave pode ser o nome da música ou do artista, e o valor
associado é a informação detalhada ou o local onde a música está armazenada.
Nas redes sociais, ao recebermos sugestões de “Pessoas que
talvez conheça”, o sistema, por trás das cenas, pode
usar conjuntos para armazenar e processar rapidamente
ID de usuários dos quais já somos amigos, garantindo que
as sugestões não incluam pessoas que já fazem parte da
nossa lista.
Interatividade
Qual das seguintes afirmações é correta sobre ordenação, filas, pilhas e dicionários?
a) Dicionários são automaticamente ordenados, enquanto vetores, filas e pilhas não são.
b) A ordenação de vetores, pilhas e filas é realizada usando o método Sort().
c) Pilhas são usadas para ordenação de dados, operando com o princípio FIFO (First In, First
Out), assim como as filas.
d) Os vetores podem ser ordenados usando algoritmos de
ordenação personalizados, filas operam com o princípio
FIFO, pilhas com o princípio LIFO e dicionários não
têm ordenação.
e) Filas, pilhas e dicionários são estruturas de dados que
possuem métodos integrados para ordenação, semelhantes
aos vetores.
Resposta
Qual das seguintes afirmações é correta sobre ordenação, filas, pilhas e dicionários?
a) Dicionários são automaticamente ordenados, enquanto vetores, filas e pilhas não são.
b) A ordenação de vetores, pilhas e filas é realizada usando o método Sort().
c) Pilhas são usadas para ordenação de dados, operando com o princípio FIFO (First In, First
Out), assim como as filas.
d) Os vetores podem ser ordenados usando algoritmos de
ordenação personalizados, filas operam com o princípio
FIFO, pilhas com o princípio LIFO e dicionários não
têm ordenação.
e) Filas, pilhas e dicionários são estruturas de dados que
possuem métodos integrados para ordenação, semelhantes
aos vetores.
Dicionários e conjuntos
Com ela, é possível associar uma chave única a um valor específico, e essa associação
garante que os dados sejam recuperados de maneira rápida, aproveitando as características
subjacentes das tabelas de hash.
A principal característica dos conjuntos é que eles armazenam itens únicos, ou seja, não
permitem duplicatas, tornando-os ideais quando se deseja manter uma coleção de itens
distintos, como uma lista de IDs de usuários únicos.
A classe HashSet garante que os itens sejam armazenados e acessados de forma eficiente,
também aproveitando os princípios das tabelas de hash.
São uma das abordagens mais interessantes para organizar informações de forma
hierárquica e têm uma presença marcante, especialmente quando se trata de
armazenamento e recuperação eficientes de dados.
Em uma árvore binária, cada nó tem no máximo dois filhos, muitas vezes denominados
subárvore esquerda e subárvore direita. Uma característica importante de algumas árvores
binárias é a forma como são organizadas.
Quando se tem uma árvore de busca binária, por exemplo, os valores são dispostos de tal
forma que, para qualquer nó dado, os valores à esquerda são menores que o valor do nó, e
os valores à direita são maiores. Essa disposição permite uma busca eficiente, pois elimina
metade das opções possíveis a cada passo.
Uma preocupação ao usar árvore de busca binária é seu
equilíbrio. Se os dados forem inseridos em uma ordem
particular, ela pode ficar muito inclinada, assemelhando-se
mais a uma lista vinculada. Essa situação elimina os
benefícios de tempo de busca da estrutura de árvore.
É aqui que entram as árvores autobalanceadas.
Árvores
A biblioteca-padrão não fornece uma implementação direta dessas árvores, mas estruturas
como SortedSet<T> e SortedDictionary<TKey, TValue> se baseiam em árvores e oferecem
características de desempenho semelhantes.
São um tipo especial de árvore binária (ou, em alguns casos, não binária) que segue
determinadas propriedades. O heap binário é, talvez, o mais familiar de todos. Trata-se de
uma árvore binária completa, em que cada nó tem um valor que respeita uma relação
específica se comparado aos valores.
Os heaps binomiais são uma extensão interessante da ideia dos heaps binários – uma
coleção de árvores binomiais, em que cada árvore é um heap.
A peculiaridade dos heaps binomiais é permitir operações como união de dois heaps de
maneira eficiente, num tipo de heap formado pela combinação de outros heaps binomiais
menores, seguindo propriedades específicas que garantem sua eficiência.
Os heaps de Fibonacci são uma coleção de árvores que não necessariamente precisam ser
binárias e que são muito hábeis em certas operações, como diminuir uma chave ou excluir
um nó em tempo amortizado constante.
São estruturas de dados que podem representar relações binárias entre um conjunto de
objetos e são formados por vértices (nós), conectados por arestas ou arcos.
São amplamente usados para resolver problemas e modelar sistemas em campos variados,
como redes de computadores, sistemas de transporte e redes sociais.
Em essência, são instrumentos matemáticos extremamente valiosos e versáteis, aplicáveis
em diversas situações cotidianas e profissionais, servindo como base para modelar e
resolver uma variedade de problemas práticos em diferentes domínios.
Frequentemente, representam relações entre objetos ou entidades, proporcionando uma
representação visual e estrutural de sistemas complexos.
Grafos
O algoritmo de Kruskal é uma técnica que encontra a árvore geradora mínima em um grafo
(Kruskal, 1956), ou seja, um subconjunto do grafo que conecta todos os vértices com o
menor custo total. Ele começa tratando cada vértice como componente separado e,
iterativamente, seleciona a aresta de menor peso que conecta dois componentes distintos,
continuando esse processo até que todos os vértices estejam conectados.
O algoritmo de Prim, por outro lado, também encontra a árvore geradora mínima em um
grafo, mas adota uma abordagem diferente do Kruskal; ele começa a partir de um vértice
arbitrário e, a cada passo, adiciona ao conjunto a aresta de menor peso que conecta um
vértice dentro do conjunto a um vértice fora dele, continuando o processo até que todos os
vértices estejam no conjunto.
O algoritmo de Dijkstra encontra o caminho mais curto entre
dois vértices em um grafo ponderado, em que arestas têm
pesos associados a elas, que podem representar, por
exemplo, distâncias ou custos. Esse algoritmo trabalha de
maneira incremental, começando do vértice de origem e
expandindo-se para os vértices vizinhos, atualizando as
distâncias conhecidas, até alcançar o vértice de destino.
Grafos
Em um grafo direcionado, cada aresta tem uma direção, indo de um vértice origem para um
vértice destino. Em um grafo não direcionado, as arestas não têm direção.
Grafos ponderados têm valor ou peso associado a cada aresta, representando, por exemplo,
a distância entre dois pontos ou o custo para percorrer uma conexão. Grafos não
ponderados não têm pesos associados às suas arestas.
Qual das seguintes afirmações é correta sobre a implementação de árvores, heaps e grafos?
Qual das seguintes afirmações é correta sobre a implementação de árvores, heaps e grafos?