Manipulando strings

Existem diversos métodos no próprio tipo string para sua manipulação. No entanto, as strings são imutáveis, ou seja, não podem ser alteradas uma vez criadas. Até é possível criar outra string, mas não mudá-las. Nos casos em que seria conveniente modificar a string, podemos usar a classe “StringBuilder”. Veja o exemplo a seguir:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using System;
using System.Text;
 
public class Class1
{
      // Troca um caractere por outro dentro da string
      static void Troca(ref StringBuilder s, char Chl, char Ch2)
      {
            for(int i = 0; i < s.Length; i++)
            {
                  if (s[i] == Chl)
                        s[i] = Ch2;
            }
      }
 
      public static void Main()
      {
            // Cria a string
            StringBuilder s = new StringBuilder("Maria tinha um carneirinho");
            // Troca caracteres
            Troca(ref s, ‘ ‘, ‘_’);
            // Exibe
            Console.WriteLine(s);
      }
}

O classe string possui uma série de métodos que auxiliam na sua manipulação, entre eles:

  • Substring
  • IndexOf
  • ToUpper
  • ToLower
  • StartsWith
  • EndsWith
  • TrimStart
  • TrimEnd
  • Trim
  • PadLeft
  • PadRight
  • Join
  • Split
  • Format

Estas serão vistas mais adiante com detalhes.

Class (por referência)

Permite declarar classes, de maneira semelhante ao C++. As principais diferenças são:

  • Não é preciso seguir uma ordem de declaração, podendo declarar variáveis por exemplo, em qualquer parte do corpo da classe
  • Não é permitida a herança múltipla de classes

Um exemplo simples de uma classe em C# pode ser visto abaixo:

1
2
3
4
class MinhaClasse
{
    // Corpo da classe (Métodos, Variáveis, Propriedades, sub classes)
}

Veja maiores detalhes no capítulo sobre Orientação a Objetos que será abordado posteriormente neste curso.

Struct (por valor)

Permite declarar tipos que contém vários valores, identificados por um nome. Semelhante a struct do C. Exemplo:

1
2
3
4
5
6
7
8
9
public struct Point
{
    public int x, y;
    public Point(int pl, int p2)
    {
          x = pl;
          y = p2;
    }
}

As structs possuem algumas características em comum com as classes:

  • Podem ter métodos.
  • Podem ter construtores.

Entretanto, existem diferenças em relação às classes:

  • Elas são tipos por valor enquanto classes são tipos por referência
  • Não podemos declarar um construtor que não aceite argumentos.
  • Podemos atribuir à variável “this”.
  • Não suportam herança; elas são implicitamente “sealed”.

As structs fornecem uma alternativa mais “leve e barata” às classes, onde o custo do uso das classes (alocação dinâmica de memória, métodos virtuais e uso de ponteiros) seria muito caro. Por exemplo, um ponto (coordenada X, Y).

Tipos Definifidos pelo Usuário

Com o C# você pode declarar novos tipos, de maneira semelhante ao C++ e Pascal. Os novos tipos são usados em qualquer situação que normalmente você usaria um tipo.

Diversos mecanismos são disponibilizados para criação de tipos personalizados, desde os tradicionais já disponibilizados pelo natural suporte à orientação a objetos, até mecanismos particulares da linguagem, como:

  • Enum
  • Struct
  • Class
  • Interface
  • Arraylists

Todos serão vistos com mais detalhes no decorrer do curso.

Unboxing

Para “voltar” o boxing, precisamos fazer um cast. Observe o exemplo abaixo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
using System;
 
public class Class1
{
      public static void Main()
      {
            int N1 = 10;
            object O;
            O = N1;                            // Boxing
            N1 = (int)O;                       // Unboxing string S1;
            Sl = "Valor:" + ((int)O).ToString(); // Unboxing S1.ToUpper();
            Console.WriteLine(S1);
      }
}
...

Caso o “cast” seja inválido, uma exception é gerada.

Boxing

Todos os tipos do C# podem ser considerados derivados de “object”. Isto quer dizer que mesmo os tipos simples têm propriedades e métodos. Normalmente, os tipos por valor não têm um ponteiro “this”, “me” ou “self” para ser passado a um método. O que acontece é que o C# faz isso automaticamente. Por exemplo, veja o seguinte código:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
using System;
public class Class1
{
      public static void Main()
      {
            int N1 = 10;
            string S1;
            S1 = "Valor:" + N1.ToString();
            S1.ToUpper();
            Console.WriteLine(Sl);
      }
}
...

Quando chamamos N1.ToString(), acontece o seguinte:

  • É alocado um inteiro no “heap”. O ponteiro retornado é usado na chamada do método ToString() como “this”;
  • O valor de N1 é copiado para esta área recém alocada;
  • O ponteiro é desalocado automaticamente dentro do mecanismo de “coleta de lixo”;

Sistema de Tipos Unificado

Em C# todo tipo pode ser visto como um objeto. Isto quer dizer basicamente o seguinte:

  • Qualquer expressão, de qualquer tipo, pode ser atribuída a uma variável do tipo object;
  • Qualquer expressão, de qualquer tipo, tem propriedades e métodos;

Os tipos “por referência”, pela própria natureza, são referências (“ponteiros domesticados”) e não implicam em um custo a mais ao terem propriedades e métodos. Já os tipos por valor, em princípio, não carregam nenhum custo adicional. Eles são automaticamente convertidos para referências quando necessário em um processo conhecido como “boxing” que será explicado adiante.

Como já foi comentado, o tipo base que promove toda a unicidade deste sistema de tipos é o Object. Relembrando, todos os tipos criados herdam implicitamente de object e podem ser representados por ele, ou seja, object pode receber qualquer outro tipo, inclusive primitivos (por valor).

Conversões

Você pode usar tipos numéricos diferentes em expressões. Neste caso, as seguintes conversões ocorrem:

  • Quando misturados tipos inteiros e ponto flutuante, os tipos numéricos são convertidos para ponto flutuante.
  • Os tipos inteiros de menor faixa são convertidos para os de maior faixa, por exemplo, de short para int.

As conversões são feitas automaticamente apenas quando o tipo a receber o valor (do lado esquerdo de uma atribuição) puder conter todos os possíveis valores da expressão (do lado direito de uma atribuição). Muitas conversões não são permitidas, como por exemplo:

  • Entre tipos com e sem sinal de variáveis inteiras do mesmo tamanho (por exemplo, entre int e uint).
  • Atribuições de variáveis inteiras com sinal para inteiros sem sinal, qualquer que seja o seu tamanho.
  • Entre o tipo decimal (de grande resolução) e ambos os tipos de ponto flutuante IEEE, double e float (de maior faixa).
  • Atribuir um double a um float.
  • Atribuir números de ponto flutuante a variáveis inteiras.

Caso você precise fazer uma conversão na qual exista perda de informação, você pode forçá-la com um operador de conversão, como no exemplo abaixo:

1
2
3
4
5
6
7
...
long   myLong = 22;
int   myInt = myLong;<span> </span>// Error 
int   myInt = <strong>(int)</strong>myLong; // OK
int   x = 3.0;<span> </span>// Error
int   y = (int)3.0;<span> </span>// OK
...

As conversões automáticas entre os tipos por valor são as seguintes:

De

Para

sbyte

short, int, long, float, double, ou decimal.

byte

short, ushort, int, uint, long, ulong, float, double, ou decimal.

short

int, long, float, double, ou decimal.

ushort

int, uint, long, ulong, float, double, ou decimal.

int

long, float, double, ou decimal.

uint

long, ulong, float, double, ou decimal.

long

float, double, ou decimal.

ulong

float, double, ou decimal.

char

ushort, int, uint, long, ulong, float, double, ou decimal.

float

double.

Decimal (por valor)

Este tipo está bem menos sujeito a erros de arredondamento que ocorrem com os formatos binários. As contas são feitas na base 10

Tipo Implementação
decimal Ponto flutuante decimal de 128 bits. (1.0 x 10-28 a 7.9 x 1028), 28 dígitos decimais de precisão.

O tipo decimal é uma novidade. Ele tem 28 dígitos de precisão e as contas são menos propensas a erros de arredondamento comuns aos formatos IEEE e odiados por quem cria software de contabilidade. Por exemplo, com o decimal 14,2 mais 0,2 dá 14,4 ao invés de 14,399999999999999 que você obteria com o tipo double.

Ponto Flutuante (por valor)

Os números de ponto flutuante são bastante convencionais, usando o padrão IEEE de quatro e oito bytes.

Tipo

Implementação

double

Ponto flutuante binário IEEE de 8 bytes (±5.0 x 10-324 a ±1.7 x 10308), 15 dígitos decimais de precisão.
float Ponto flutuante binário LEEE de 4 bytes (±1.5 x 10-45 a ±3.4 x 1038), 7 dígitos decimais de precisão.

As operações de ponto flutuante não geram erros:

  • Uma operação que gere um resultado muito pequeno para ser representado torna-se zero.
  • Uma divisão por zero com números de ponto flutuante não gera erro e sim produz resultado “infinito”. Você pode verificar se o valor é infinito positivo ou negativo com o método Double.Islnfinity().
  • Uma operação inválida gera como resultado NaN (não é um número).
  • Se um dos operadores for NaN, o resultado é NaN.