Material de estudos para desenvolvimento na linguagem C#
Fundamentos do C# por Andre Baltieri da balta.io
using System;
namespace MeuApp
{
class Program
{
static void Main(string[] args)
{
console.WriteLine('Hello, World!');
}
}
}
Utilizamos a palavra chave using
para definir as importações que serão feitas dentro de um arquivo.
Serve para criar uma divisão lógica dentro de um arquivo. Geralmente os Namespaces
acompanham o nome da página. Assim como não podemos ter dois arquivos com o mesmo nome dentro de uma pasta, também não podemos ter dois Namespaces
com o mesmo nome dentro de um arquivo.
É um dado que é instanciado (criado, inicializado, etc), e do qual atribuímos um valor para depois utilizar em algum trecho do nosso código. Por "variável" entendemos que seu valor não é imutável, portanto pode ser alterado.
↓ Exemplo ↓
int idade;
int idade = 25;
var idade = 25;
var idade; // esse exemplo está errado, pois não atribuímos um valor nem inferimos o tipo da variável. devemos sempre inferir o tipo, ou no caso de "var", atribuir um valor.
Diferente de uma variável, os valores das constantes são inalteráveis. Uma vez que a gente atribui um valor à constante em sua inicialização, o valor dela não pode mais ser alterado. Elas não funcionam com a palavra chave var
e são mais otimizadas que as variáveis.
Por convenção é comum vermos as constantes sempre escritas com letras maiúsculas separadas por undescore. Ex: "IDADE_MINIMA"
.
const int IDADE_MINIMA = 25;
Os tipos primitivos possuem uma capacidade. Por exemplo, os tipos int
vão do número -2.147.483.648
até o número 2.147.483.647
(caso a gente atribua um valor que fuja desse intervalo receberemos um erro).
- Tipos simples (simple types);
- Enumeradores (enums);
- Estruturas (structs);
- Tipos nulos (nullable types)
No .NET tudo começa de um tipo base chamado system
. Todos os tipos derivam dele. Seu uso já é implicito e todos os objetos, variáveis e constantes.
O tipo byte
é usado para representar um byte de fato. Seu valor vai de 0 até 255 (8-bit).
São números sem pontuação podendo ser curtos, médios ou longos. Utilizamos a notação unsigned
para definir que as variáveis não podem receber valores negativos.
- short/ushort (16-bit)
- int/uint (32-bit)
- long/ulong (64-bit)
São números que possuem casas decimais. Para isso usamos o float, o double ou o decimal.
- float (32-bit | notação F)
- double (64-bit)
- decimal (128-bit | notação M)
A diferença entre o uso dos tipos acima está na precisão de casas decimais.
Utiliza 8-bits
de memória e armazena true
ou false
. Usamos a palavra chave bool
para inferir este tipo.
Utilizado para armazenarmos um único caractere (unicode). É definido pela palavra chave char
e a inferência de valor se dá pelas aspas simples. Ocupa 16-bit de memória.
Armazena uma cadeia de caracteres (um conjunto de chars). É definido pela palavra chave string
e a inferência de valor se dá pelas aspas duplas. O espaço em memória é definido pelo tamanho da string
.
Infere dinamicamente o tipo da variável de acordo com o valor atribuído à ele. Por exemplo, se atribuírmos o valor 25
à uma variável, logo o tipo inferido será int
. Logo após essa definição dinâmica, não é possível alterar o tipo da variável.
É similar ao "any"
do Typescript
. É um tipo genérico que recebe qualquer valor ou objeto e que não ativa o intelisense
dos editores por ser um tipo desconhecido. Na dúvida, evite usa-lo.
Diferente de uma string vazia, o null
significa vazio, nada. Todo tipo primitivo ou complexo pode receber o valor null
. Para isso deverá ser marcado como Nullable Type
👇.
int? idade = null;
Alias é o apelido que todo tipo .NET tem. Por exemplo, System.String
tem o alias string
.
int idade = 25; // Alias
Int32 idade = 25; // Tipo
Todo tipo built-in
já possui um valor padrão. Caso não seja atribuído nenhum valor à variável, ela adoratá os valores abaixo 👇
- int → 0
- float → 0
- decimal → 0
- bool → false
- char → '\0'
- string → ""
É comum transformarmos os tipos de uma determinada variável. Para isso podemos usar as Conversões Implícitas
que podem ser executadas com passagem de dados dos quais os tipos são compatíveis. Por exemplo:
float valor = 25.8F;
int outro = 25
valor = outro // Conversão Implícita
Essa conversão acima só ocorre pois um número real pode receber um número inteiro. Porém o contrário não aconteceria.
Ocorre quando os tipos não são compatíveis. É dada pelo uso do tipo entre parênteses antes da atribuição. Segue as mesmas regras anteriores.
int inteiro = 100;
uint inteiroSemSinal = (uint)inteiro; // Conversão Explícita
Toda variável do tipo built-in
possui métodos chamados de Parse
. Esses métodos são usados para converter um caractere ou string para um outro tipo qualquer. Caso haja alguma incompatibilidade, gerará um erro.
Obs.: O
Parse
converte somente strings
string valor = "100";
int inteiro = int.Parse(valor);
Ao contrário do Parse
, o Convert
é um objeto que possui métodos próprios de conversão, podendo converter vários tipos de valores e não apenas strings.
string valor = "100";
int inteiro = Convert.ToInt32(valor);
- Soma → +
- Subtração → -
- Multiplicação → *
- Divisão → /
- Resto → %
Aceitam todos os valores numéricos (short, int, decimal, float e double).
Obs.: Caso eu realize uma operação onde minha variável é do tipo
int
, porém o resultado da minha operação possui números decimais, meu resultado desconsiderará os valores depois da vírgula. Para ter precisão em minhas operações é preciso estar atento ao tipo da variável.
Utilizamos "="
para atribuir um valor à uma variável ou constante, porém, podemos utilizar uma combinação de "="
junto de um operador aritmético, como por exemplo:
x += 5
é o mesmo quex = x + 5
int x = 0; // Inicialização e atribuição
x += 5; // resultado → 5
x -= 1; // resultado → 4
x *=10; // resultado → 40
x /= 2; // resultado → 20
x++; // resultado → 21 (incremento)
x--; // resultado → 20 (decremento)
x % 3; // resultado → 2
Podemos comparar qualquer tipo de dados. A comparação sempre retorna true
ou false
- Igual
==
- Diferente
!=
- Maior que
>
- Menor que
<
- Maior ou igual que
>=
- Menor ou igual que
<=
Sempre retorna verdadeiro
ou falso
.
- AND → &&
- OR → ||
- NOT → !
Utilizado o if
e else
para tomadas de decisões. Utilizamos os operadores lógicos para compor uma condição.
Podemos também utilizar o switch
, conforme exemplo abaixo 👇
/*
• utilizado quando temos muitas decisões
• executado em cascata
• devemos parar a execução com o break
• possui uma execução padrão chamada default
*/
int valor = 1;
switch (valor)
{
case 1: Console.WriteLine("1"); break;
case 2: Console.WriteLine("2"); break;
case 3: Console.WriteLine("3"); break;
default: Console.WriteLine("4"); break;
}
Servem para percorrermos uma determinada lista e criar manipulações, utilizar os dados ou criar novas listas.
for (var i = 0; i <= 5; i++)
while (valor <= 5)
{
Console.WriteLine(valor);
valor++;
}
do
{
Console.WriteLine(valor);
valor++;
} while (valor < 5);
Podemos segmentar nosso programa em functions
também conhecidos como methods
. O main
é um exemplo de method
. As funções e métodos possuem retorno, nome e parâmetros.
static void Main(string[] args)
{
// Invocação do método
MeuMetodo("C# is nice");
}
// Definição do método
static void MeuMetodo(string parametro)
{
Console.WriteLine(parametro);
}
Obs.:
- Não conseguimos criar um método dentro de uma outra função em
C#
. Caso queiramos executar um método dentro de uma função, precisamos criar ela fora do escopo e depois instanciá-lo (caso ele não sejastatic
). - Quando identificamos um método como
static
estamos dizendo que ele não precisa ser instanciado, podendo então ser chamado diretamente. - Quando criamos uma função, sempre colocamos o retorno dela no início do método:
// nesse caso o método não retornará nada
static void MeuMetodo() {...}
// nesse caso o método retornará uma string
static string RetornaString() {...}
// nesse caso o método retorna um número e precisa ser instanciado
int RandomNumber() {...}
A memória é dividida entre Stack
e Heap
.
Artigo - Gerenciamento de memória no C#: stack, heap, value-types e reference-types
Porção de memória pequena onde os value-types e os
ponteiros
ficam;
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.
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
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
) nastack
que aponta para qual posição de memória o objeto está armazenado naheap
. O acesso é feito via essa referência nastack
. Sendo assim, o acesso ao conteúdo é indireto, dependendo dessa referência;
→ Resumidamente, Built-in, Structs e Enuns são salvos na Stack. Objetos, Classes e Arrays são salvos no Heap, porém são consultados através de uma referência salva na Stack.
→ Quando atribuímos uma variável à outra varíavel de tipo primitivo, criamos uma cópia do valor naquele momento da respectiva variável.
→ Quando criamos uma instância ou atribuímos um valor à outro array ou objeto, atribuímos o valor à referência. Sendo assim, se meu valor do objeto ou array ser alterado, a variável atribuída também será alterada pois estou usando sua referência.
// Value-Types
static void Main(string[] args)
{
int x = 25;
int y = x;
Console.WriteLine(x); // 25
Console.WriteLine(y); // 25
x = 32
Console.WriteLine(x); // 32
Console.WriteLine(y);
// ☝️ o valor continua 25 pois foi realizada uma cópia do valor da variável x naquele momento, e não uma referência
}
// Reference-Types
static void Main(string[] args)
{
var arr = new string[2];
arr[0] = "Item 1";
var arr2 = arr;
Console.WriteLine(arr[0]); // "Item 1"
Console.WriteLine(arr2[0]); // "Item 1"
arr[0] = "Item 0";
Console.WriteLine(arr[0]); // "Item 0"
Console.WriteLine(arr2[0]); // "Item 0"
/*
Ambas as listas foram alteradas pois ambas estão apontando para uma mesma referência de memória
*/
}
- Tipos de dados estruturados
- Apenas a estrutura, o esqueleto
- Value-Type
- Definido pela palavra
struct
- Composto de propriedades e métodos
- Criado a partir da palavra
new
struct Product
{
// Props
public int Id;
public string Title;
public float Price;
// Methods
public float PrinceInDolar(float dolar)
{
return Price * dolar
}
}
// Para criar uma nova estrutura basta usar a palavra "new" seguido da chamada da chamada do structure
static void Main(string[] args)
{
Product product = new Product()
// ou assim 👇
var product = new Product()
// nesse caso o tipo é opcional
product.Id = 1;
product.Title = "Mouse Gamer";
product.Price = 100;
Console.WriteLine(product.Id);
Console.WriteLine(product.Title);
Console.WriteLine(product.Price);
}
Obs.: Podemos criar uma
struct
usando também um método construtor. A diferença é que ao instanciar já devemos passar as propriedades.
struct Product
{
public Product(int id, string title, float price)
{
Id = id;
Title = title;
Price = price;
}
}
Usado para fornecer uma melhor visualização do código. Substituem o uso de inteiros e são usados em listas curtas e dados fixos.
enum EEstadoCivil
{
Solteiro = 1,
Casado = 2,
Divorciado = 3
}
struct Cliente
{
public string Nome;
public EEstadoCivil EstadoCivil;
}
var cliente = new Cliente("João Silva", EEstadoCivil.Casado);
Console.WriteLine(cliente.EstadoCivil) // Casado
Console.WriteLine((int)cliente.EstadoCivil) // 2
Strings são o que? São "letras, são números, são caracteres, e tudo mais que cabe em aspas".
São sequências de caracteres randomicas utilizadas como identificação única de um registro, usuário, produto, etc.
using System;
var id = Guid.NewGuid();
Se precisarmos usar um valor randômico basta instanciar o Guid
. Se precisarmos que o id
possua menos caracteres, basta usar o método Substring
.
id.ToString().Substring(i,q);
// sendo "i" a posição inicial e "q" a quantidade total de caracteres.
// ex: Substring(0, 8)
Conseguimos unir strings para que uma determinada mensagem seja passada de forma legível para o usuário. Podemos fazer essa interpolação de várias maneiras:
// usando o operador +
var presentation = "Meu nome é";
var name = "João da Couve";
var myPresentation = "Meu nome é" + " " + name;
// usando string.Format()
var price = 15.99;
var finalPrice = string.Format("O preço final é {0}", price);
// usando $
// obs.: podemos usar @ para escrever multiplas linhas
var helloWorld = $"Olá mundo, meu nome é {name}."
var text = $@"Era uma vez...
E a chapeuzinho vermelho...
E por fim matou o lobo...
FIM."
Podemos comparar e analisar ou verificar se uma string contém algum elemento ou pedaço de texto
var texto = "Testando";
Console.WriteLine(texto.CompareTo("Testando"));
// CompareTo ou Equals retornará um inteiro sendo -1 para false e 0 para true
Console.WriteLine(texto.Contains("Testando algo"));
// Contains informará se o que foi passado como parâmetro existe na variável, retornando um boolean
Console.WriteLine(texto.StartsWith("Este"));
// Verifica se a string começa com o parâmetro retornando um boolean
Console.WriteLine(texto.EndWith("por fim."));
// Verifica se a string termina com o parâmetro retornando um boolean
Console.WriteLine(texto.Equals)
Como segundo parâmetro dos métodos apresentados existe o objeto StringComparison
que possui propriedades das quais é possível informar o nível de comparação que desejamos.
Diz respeito à posição de um caracter ou conjunto de caracter em uma lista/string
var texto = "Este texto é uma lista de caracteres"
Console.WriteLine(texto.IndexOf("lista"))
// Retornará um número informando em qual posição o parâmetro está.
// Assim que encontrar pela primeira vez o parâmetro, retornará a posição dele
// Os índices iniciam em zero
Console.WriteLine(texto.LastIndexOf("s"))
// Retornará um número informando em qual posição o parâmetro está
// Porém considera sempre a posição do último caracter encontrado ao invés do primeiro como no método IndexOf()
var texto = "Isso é um teste"
Console.WriteLine(texto.toLowerCase()) // todas as letras minúsculas
Console.WriteLine(texto.toUpperCase()) // todas as letras maiúsculas
Console.WriteLine(texto.Insert(5, "aqui ")) // insere texto na posição definida
Console.WriteLine(texto.Remove(5, 5)) // remove um texto de um tamanho definido na posição definida
Console.WriteLine(texto.Length) // retorna a quantidade de caracteres de uma string/lista
Console.WriteLine(texto.Replace("Isso", "Este")) // substitui os caracteres ou textos da string
var splitTexto = texto.Split(" ") // transforma a string em uma lista, cujos elementos são separados a partir do caracter informado nos parâmetros
var substringTexto = texto.Substring(5, 5) // pega os caracteres a partir do índice e com a quantidade de caracteres definido
var trimTexto = texto.Trim() // remove os espaços no início e fim de uma string
Podemos incrementar uma variável do tipo string fazendo
variavelString += " um pouco mais de texto"
, porém tal prática faz com que seja criada uma nova cópia da variável em cada incremento. Dependendo da complexidade e tamanho da apliação isso não é nada bom para a memória. Uma opção melhor seria usar o StringBuilder conforme abaixo:
var texto = new StringBuilder();
texto.Append("Meu nome é fulano.");
texto.Append(" Eu nasci a dez mil anos atrás.");
texto.Append(" E possuo miopia.");
Fazendo isso, ao invés de criar uma cópia da variável à cada incremento, estaremos adicionando caracteres à um objeto. Para usar a variável como uma string é só utilizar o texto.ToString()
.
A execução de um programa em C# funciona da seguinte maneira:
- O programa busca o
Program.cs
- Lê a classe
Program
- Lê e executa o método
Main
- Durante a execução, todos os arquivos serão unificados em uma
DLL
- A divisão física se perderá
- Sobrará apenas a divisão lógica, que são os
Namespaces
dotnet new console -o MyNewProject