C#

STRINGHE

Altro argomento portante relativo a C# (e .Net) è quello relativo alle stringhe. La parola chiave che le definisce è string, alias per System.String, la classe da cui originano. Nella pratica si tratta di una sequenza finita (la lunghezza minima è zero corrispondete alla stringa vuota, quella massima praticamente possibile dovrebbe essere 2^32 / 2 ma vi auguro di non avere mai bisogno di arrivare a tanto...) indicizzata e immutabile di caratteri Unicode. Il range dei caratteri ammissibili va da U+0000 a U+FFFF, come consueto in molti linguaggi. Il concetto di immutabilità è molto semplice, nella pratica esso significa che qualsiasi modifica (tipo aggiungere o togliere o sostituire un elemento) applicata ad una stringa in realtà ne crea una nuova, non cambia quella preesistente. Vedremo come questo possa impattare sull'efficienza del codice. Fornisco subito un'informazione che spesso non viene data: quanto "pesa" una stringa? Per ognuna di esse vengono allocati 20 bytes per i dati relativi all'oggetto in sè, + 2 bytes per ogni suo elemento interno. Quindi:

20 + (2 * Length) bytes (vedremo presto cosa rappresenta Length)

Da un punto di vista fomale le stringhe sono contenute all'interno di una coppia di doppi apici, secondo uno schema anche questo del tutto comune. Esempi di stringa quindi possono essere i seguenti:

string s1 = "ciao!"
string s2 = "" // la stringa vuota
string s3 = "W la Juventus"
string s4 = "1234" // non è un numero essendo all'interno di una coppia di doppi apici
string s5 = "a  ( # p"

ecc....

L'importanza delle stringhe è abbastanza facile da comprendere, tenete presente che tutti gli oggetti in .Net dispongono del metodo ToString() per la conversione in stringa.
Le stringhe sono reference type ovvero creare una stringa significa allocare qualcosa nello heap, in modo del tutto simile agli array. La classe System.String è sealed, (riparleremo di questo attributo quando analizzeremo le classi in dettaglio) il che significa in pratica che da essa non si può ereditare. Il motivo è che permettere di ereditare direttamente da questa classe porterebbe alla nascita di tante varianti di stringhe caratterizzate da importanti modifiche semantiche mentre si vuole che questo elemento del framework sia solido e monolitico permettendo importanti ottimizzazioni per il suo trattamento a runtime. A questo proposito va ricordato che il CLI memorizza le stringhe in una sorta di tabella chiamata pool interno che viene interrogata alla creazione di ogni stringa per vedere se essa esiste già e quindi appendervi eventualmente un ulteriore riferimento. Lo scopo principale è la riduzione di occupazione di memoria. Come vedremo esiste un modo per interfacciarsi con tale tabella. Il fatto che le stringhe siano un po' speciali per il CLI lo possiamo intuire anche da questo banale codice analizzato attraverso il solito ILDASM:

public static void Main()
{
  string s1 = "prova";
}

che dà origine a questo codice CLI:

.method public hidebysig static void Main() cil managed
{
.entrypoint
// Code size 8 (0x8)
.maxstack 1
.locals init (string V_0)
IL_0000: nop
IL_0001: ldstr "prova"
IL_0006: stloc.0
IL_0007: ret
} // end of method Test::Main


In rosso ho evidenziato l'istruzione chiave. In effetti ldstr sta per load string, un'istruzione specifica ed esclusiva per le stringhe. Interessante anche andare a vedere cosa succede quando si cerca di modificare la stringa stessa:

string s1 = "1234";
s1 = "12345";


questo codice inserito al posto di quello di quello immediatamente qui sopra genera:

.method public hidebysig static void Main() cil managed
{
.entrypoint
// Code size 14 (0xe)
.maxstack 1
.locals init (string V_0)
IL_0000: nop
IL_0001: ldstr "1234"
IL_0006: stloc.0
IL_0007: ldstr "12345"
IL_000c: stloc.0
IL_000d: ret
} // end of method Test::Main

vedete? ldstr viene chiamato due volte, ovvero vengono generate due stringhe diverse, ed s1 punterà prima a "1234" e poi a "12345" mentre la zona di memoria che ospita la prima stringa sarà ripulita dal garbage collector.

Le definizioni viste sopra costituiscono il modo tipico di istanziazione delle stringhe. In pratica si usa l'alias string, seguito dal nome che vogliamo dare alla nostra stringa e quindi assegnando un valore iniziale che può essere anche vuoto. In realtà sono definiti ben 8 costruttori ma 5 di essi sono usati in ambito unsafe e non li trattiamo qui. Gli altri sono:

string s1 = new String(char[]);
string s2 = new String(char[], int32, int32);
string s3 = new String(char, int32);

vediamo un esempio che riprende queste definizioni:

  Esempio 12.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;
public class Test
{
  public static void Main()
  {
    char[] a1 = {'a','b','c','d','e'};
    string s1 = new String(a1);
    string s2 = new String(a1,2,3);
    string s3 = new String('a',5);
    Console.WriteLine(s1);
    Console.WriteLine(s2);
    Console.WriteLine(s3);
  }
}

Con questo output che andiamo a spiegare:

abcde
cde
aaaaa

Alla riga 6 definiamo un array di caratteri.
Alla riga 7, questo è interessante, costruiamo una stringa partendo da questo array.
Alla riga 8 definiamo una stringa a partire da questo array limitandoci a 3 caratteri a partire dal'indice 2
Alla riga 9 costruiamo una stringa ripetendo per 5 volte il carattere 'a'.

Si tratta di 4 sistemi di definizione che useremo poco ma non sono privi di interesse, in particolare abbiamo visto un sistema per costruire una stringa partendo da un array di caratteri.

Le stringhe hanno molte similitudini con gli array. In particolare è possibile accedere ad un singolo elemento di un una stringa tramite il già noto operatore []. L'indicizzazione è 0-based, come avviene per gli array. Tuttavia ci sono alcune differenze sostanziali, come vedremo tra breve. Per intanto vediamo un semplice esempio di partenza che illustra i concetti appena visti:

  Esempio 12.2
1
2
3
4
5
6
7
8
9
10
using System;
public class Test
{
  public static void Main()
  {
    string s1 = "prova";
    for (int i = 0; i < s1.Length; i++)
    Console.Write(s1[i] + " ");
  }
}

La riga 7 ci permette di osservare l'indice 0 come punto di partenza per la accedere agli elementi della nostra stringa mentre è presente anche la proprietà length che, come per gli array, ci restituisce la lunghezza, ovvero il numero di elementi della stringa. La riga 8 invece ci conferma come l'accesso al singolo elemento avvenga attraverso l'operatore [] già citato. Da notare che, diversamente da quanto avviene per gli array, per le stringhe non sono ammessi indici negativi, i valori accettati sono solo quelli positivi a partire a 0. Approfittiamo di quanto appena visto per fare un'altra verifica; le sitruzioni seguenti

string s1 = "1234";
Console.WriteLine(s1[2].GetType());


ci restituiscono

System.Char

a testimonianza del fatto che ogni eleemnto di una stringa, anche se sembra un numero come in questo caso, è un carattere, come detto in sede di definizioni delle stringhe.

Una ulteriore, importante, differenza comportamentale rispetto agli array la incontriamo nell'esempio seguente:

  Esempio 12.3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;
public class Test
{
  public static void Main()
  {
    string s1 = "prova";
    string s2 = s1;
    Console.WriteLine(s1);
    Console.WriteLine(s2);
    s1 = s1 + "-2";
    Console.WriteLine(s1);
    Console.WriteLine(s2);
  }
}

Che ha il seguente output:

prova
prova
prova-2
prova

E' la dimostrazione dell'indipendenza delle stringhe. La modifica apparentemente messa in atto su s1 non ha effetto su s2 e questo perchè s1, essendo immutabile, come detto in precedenza, viene in realtà ricreata e viene creato contestualmente un puntatore ad essa mentre quello di s2 continua a puntare alla locazione di memoria che contiene la stringa "prova". Diversamente da quanto visto per gli array possiamo quindi effettuare le nostre modifiche senza temere il propagarsi delle stesse. L'immutabilità ha anche un impatto positivo sui processi di sincronizzazione, come è intuibile. Naturalmente questa modo di operare ha delle controindicazioni. Supponiamo di avere un programma in cui molte stringhe devono essere modificate; è ovvio constatare che ben presto lo heap sarà pieno di ex stringhe non più puntare da nulla e che aspettano solo che il garbage collector le spazzi via. Inoltre stringhe molto lunghe che dabbano essere riscritte anche per piccole modifiche comportano un qualche overhead. Insomma, se vi trovate in queste situazioni potreste dover cercare una soluzione alternativa, che esiste e che incontreremo più avanti in questo stesso paragrafo.

Nell'ambito delle stringhe potete usare i caratteri di escape per introdurre una qualche forma di formattazione. Vediamo qualche esempio:

  Esempio 12.4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;
public class Test
{
  public static void Main()
  {
    string s1 = "ciao\nmondo";
    string s2 = "ciao\tmondo";
    string s3 = "C:\\Windows\\System";
    string s4 = @"C:\Windows\System";
    Console.WriteLine(s1);
    Console.WriteLine(s2);
    Console.WriteLine(s3);
    Console.WriteLine(s4);
  }
}

l'output è questo:

ciao
mondo
ciao mondo
C:\Windows\System
C:\Windows\System

La stringa definita alla riga 6 contiene la sequenza di escape \n che significa, pratica "a capo"
Alla riga 7 vediamo allora \t ovvero una tabulazione
La riga 8 ci mostra come possiamo inserire dei caratteri che altrimenti sarebbero interpretati come introduzione ad una sequenza di escape, la  \\ permette infatti di visualizzare il carattere \. Questo vale anche per altri caratteri, ad esempio se voleste stampare il carattere ", che di per se chiuderebbe la stringa, potete scrivere \".
La riga 9 invece è la cosidetta verbatim string, che elimina il discorso delle sequenze di escape.
Le sequenza di escape sono facilmente reperibili ovunque, di seguito un esempio che usa /r, ovvero carriage returne, una squenza che causa il riposizionamento del cursore ad inizio riga senza andare a capo, come avviene invece con il \n, come abbiamo visto. L'esempio è un rozzo "contatore":

  Esempio 12.5
1
2
3
4
5
6
7
8
9
10
11
12
13
using System;
public class Test
{
  public static void Main()
  {
    int x = 0;
    while (x < 1000000)
    {
      Console.Write(x + "\r");
      x++;
    }
  }
}

Uno dei vantaggi, prevedibile, del lavorare con le stringhe, è il poter usufruire degli innumerevoli metodi e proprietà precotte che il framework ci mette a disposzione. Il seguente elenco è un po' arido, come lo sono tutti quelli di questo tipo, ma conoscerlo per bene e saper usare quanto in esso contenuto vi darà senza dubbio dei vantaggi, in futuro. Sono veramente tante, e non sono nemmeno tutte, ma ribadisco la sua utilità, almeno uno sguardo datelo, di seguito evidenzierò gli aspetti che mi paiono maggiormente degni di nota. Tenete presente che sul sito MSDN ci sono tutti gli approfondimenti del caso.

 

Clone

Restituisce un riferimento all‟istanza corrente della stringa. In pratica ne fa una copia.  

using System;
class program
{
    public static void Main()
    {
        string s1 = "ciao mondo";
        string s2 = (string)s1.Clone();
        Console.WriteLine(s2);
    }
}

Compare

Statico, confronta un due stringhe e restituisce un intero che indica il risultato del confronto. Il risultato è 0 se le due stringhe sono identiche, -1 se la seconda è maggiore 1 se la prima è maggiore. Esistono molte varianti di questa istruzione. Qui presento le due versioni più comuni:

using System;
class program
{
    public static void Main()
    {
        string s1 = "aaaaa";
        string s2 = "aaaaaa";
        string s3 = "aaaa";
        string s4 = "aaaaA";
        Console.WriteLine(String.Compare(s1, s2));
        Console.WriteLine(String.Compare(s1, s3));
        Console.WriteLine(String.Compare(s1, s4));
        Console.WriteLine(String.Compare(s1, s4, true));
   }
}  

I primi 3 confronti sono banali. Il quarto invece è nel formato
Compare(s1, s2, bool)
dove, se viene messo il valore true, viene ignorato il problema della maiuscola / minuscola.

CompareOrdinal

Statico, confronta due stringhe attraverso il confronto dei valori numerici dei singoli char componenti.

CompareTo

Anche qui viene effettuato un confronto da due stringhe con un output del tipo -1 se la seconda stringa è maggiore, 0 in caso di uguaglianza e 1 se è maggiore la prima

using System;
class program
{
    public static void Main()
    {
        string s1 = "aaaaa";
        string s2 = "aaaaa";
        string s3 = "aaaa";
        string s4 = "aaaaA";
        Console.WriteLine(s1.CompareTo(s2));
        Console.WriteLine(s1.CompareTo(s3));
        Console.WriteLine(s1.CompareTo(s4));
   }
}  

L‟output è 0 1 -1

Concat

Statico. Concatena due stringhe. Vale esattamete come il +, al massimo è più descrittivo ma non vi è vantaggio alcuno nell‟usare una forma piuttosto che l‟altra. Ci sono varie forme nel segnalo due:

la prima concatena due stringhe, la seconda tutti gli elementi di un array di stringhe.

using System;
class program
{
    public static void Main()
    {
       string s1 = "Ciao,"; 
       string s2 = " mondo";
       string[] ar1 = {"Ciao, ", "mondo"}; 
       Console.WriteLine(string.Concat(s1, s2));
       Console.WriteLine(string.Concat(ar1));
   }
}  

L‟output è sempre la stringa “Ciao, mondo”

Concat(T)

Generalizzazione del caso precedente provvede una concatenazione di una implementazione di IEenumerable(T)

Contains

Restituisce true o false a seconda che una stringa sia contenuta in un‟altra oppure no.  

using System;
class program
{
    public static void Main()
    {
       string s1 = "Ciao,";
       string s2 = "ao";
       bool b = s1.Contains(s2);
       if (b) Console.WriteLine("Ok!");
    }
}

Copy

Statico, crea una nuova istanza della stringa  

using System;
class program
{
    public static void Main()
{
        string s1 = "Ciao";
        string s2 = string.Copy(s1);
        Console.WriteLine(s2);
    }
}

 

CopyTo

Copia un certo numero di carattere da una stringa partendo da una certa posizione in un array di caratteri a partire anche qui da un dato indice. Il formato e:

CopyTo(indice di partenza, array di destinazione, indice di partenza nell‟array, numero di caratteri)

using System;
class program
{
    public static void Main()
    {
       string s1 = "XXXCiao";
       char[] ar1 = {'x','x','x','x',' ','2','0','1','5'};
       s1.CopyTo(3, ar1, 0, 4);
       foreach (char c in ar1)
       Console.Write(c);
    }
}

Il risultato è:

Ciao 2015

EndsWith

Nella sua forma basilare verifica se una stringa termina con una certa sottostringa

using System;
class program
{
    public static void Main()
    {
       string s1 = "abcde"; 
       if (s1.EndsWith("de"))
       Console.WriteLine("ok");
    }
}

Equals

Statico, booleano, può essere usato per confrontare l‟eguaglianza di due stringhe.

string s1 = "1234";
string s2 = "1234";
string s3 = "123";
Console.WriteLine(s1.Equals(s2));
Console.WriteLine(s1.Equals(s3));

risultato:

True
False

Format

Ne parleremo in apposita sezione

GetEnumerator

Recupera un oggetto in grado di iterare tra i caratteri della stringa.

GetHashCode

Restituisce il codice hash per istanza della stringa

GetType

GetTypeCode

Restituiscono il tipo. Ad esempio se s1 è una stringa, l‟output di

         Console.WriteLine(s1.GetType());
    Console.WriteLine(s1.GetTypeCode());

sarà rispettivamente:

System.String
String

IndexOf

Utile istruzione che ci restituisce la prima occorrenza di un carattere o di una stringa di caratteri. 

using System;
class program
{
    public static void Main()
    {
       string s1 = "abcde";
       Console.WriteLine(s1.IndexOf("cd"));
       Console.WriteLine(s1.IndexOf('e'));
       Console.WriteLine(s1.IndexOf("aa"));
    }
}

Questo programma ha come output il numero 2, ovvero di partenza della stringa “cd” all‟interno di “abcde”,  il numero 4, cioè l‟indice di „a‟ e infine -1 perchè la stringa “aa” non esiste in “abcde”. In questo senso IndexOf può essere usato per verificare l‟esistenza di un carattere o di una sottostringa; se il risultato è -1 quanto cercato non c‟è

IndexOfAny

Restituisce l‟indice della prima occorrenza degli elementi di un array di caratteri Detto così, come da definizione, non si capisce niente. Vediamo un esempio:

using System;
class program
{
    public static void Main()
    {
        string s1 = "abcde";
        char[] a1 = { '4', 'r', 'b', 'd' };
        char[] a2 = { '4', 'r', 'k', 'd' };
        Console.WriteLine(s1.IndexOfAny(a1));
        Console.WriteLine(s1.IndexOfAny(a2));
    }
}

L‟output è il seguente:

1
3

Ovvero:
l'indice di 'b' in s1 è 1, 'b' è il primo elemento di a1 che si trova in s1
l'indice di 'd', sempre in s1, è 3 'd' è il primo elemento di a2 che troviamo in s1. 
Utile istruzione per effettuare delle ricerche in una stringa. Abbiamo 3 sintassi differenti:

1)    IndexOfAny(array)

2)    IndexOfAny(array, intero) in cui intero indica il punto di partenza della ricerca nell‟ambito della stringa

3)    IndexOfAny(array, intero1, intero2) come al punto 2 solo che la ricerca parte da intero1 e coinvolge intero2 caratteri partendo da lì.

Insert

Inserisce in una stringa un‟altra stringa a partire da una certa posizione.

using System;
class program
{
     public static void Main()
    {
        string s1 = "Jutus";
         s1 = s1.Insert(2, "ven");
         Console.WriteLine(s1);
  }
}

Indovinate cosa esce?

Intern

Statico, recupera il riferimento interno per la stringa nell‟ambito della intern-pool. Utile per recuperare una stringa dalla tabella interna anche qualora non sia stata memorizzata tramite una stringa ma, ad esempio, via StringBuilder.

IsInterned

Recupera il riferimento interno ad una certa stringa

IsNormalized

Indica se la stringa è in una particolare forma di normalizzazione Unicode

IsNullOrEmpty

Statico, verifica se una stringa è null o vuota.

using System;
class program
{
    public static void Main()
    {
        string s1 = "abcde";
        string s2 = "";
        string s3 = null;
        Console.WriteLine(String.IsNullOrEmpty(s1));
        Console.WriteLine(String.IsNullOrEmpty(s2));
        Console.WriteLine(String.IsNullOrEmpty(s3));
    }

Ovviamente s1 non è nè null nè empty al contrario delle altre due.

IsNullOrWhiteSpace

Statico, verifica se una stringa è null o è costituita solo da blanks, molto similmente a IsNullOrEmpty.

Join

Statico, consente di concatenare gli elementi di un array o di una collezione inserendo eventualmente un separatore. Il formato è duplice:

Join(separatore, array)
Join(separatore, array, indice di partenza nell’array, numero elementi)

using System;
class program

    public static void Main()
    {
        string s1 = "";
        string s2 = "";
        string sep = ":";
        string[] a1 = {"aa", "bb", "cc", "dd", "ee"};
        s1 = String.Join(sep, a1);
        s2 = String.Join(sep, a1, 1, 2);
        Console.WriteLine(s1);
        Console.WriteLine(s2);
    }
}

L‟output è il seguente:

aa:bb:cc:dd:ee
bb:cc

LastIndexOf

Restituisce l‟indice dell‟ultima occorrenza di un carattere o il primo indice dell‟ultima occorrenza di una sottostringa. Consiglio di dare uno sguardo su MSDN alle diverse varianti di questa istruzione.

using System;
class program
{
    public static void Main()
    {
        string s1 = "aabbaabbaa";
        Console.WriteLine(s1.LastIndexOf('a'));
        Console.WriteLine(s1.LastIndexOf("aa"));   
    }
}  

L‟output è costituito dal numero 9 (indice dell‟ultima "a‟ in s1) e 8 (primo indice dell‟ultima occorrenza della sottostringa “aa” in s1)

LastIndexOfAny

Restituisce l‟indice dell'occorenza dell‟ultimo elemento di un array presente nella stringa.

using System;
class program
{
    public static void Main()
    {
        string s1 = "abcdefgh";
        char[] a1 = { 'x', 't', 'a', 'e' };
        Console.WriteLine(s1.LastIndexOfAny(a1));
    }
}  

Il risultato è 4, indice di „e‟ nella stringa

MemberWiseClone

Restituisce una shallow copy della stringa

Normalize

Restituisce la stringa in formato binario normalizzato. Ne riparleremo

PadLeft - PadRight

Permettono di allineare una stringa a sinistra o a destra per tramite di spazi o di un carattere predefinito.

using System;
class program
{
    public static void Main()
    {
        string s1 = "abcdef";
        Console.WriteLine(s1.PadLeft(9));
        Console.WriteLine(s1.PadRight(9, '*'));
        Console.ReadLine();
    }
}

L‟output è il seguente:

Tramite PadLeft la stringa è allineata verso sinistra di 3 posizioni (9 – 6, che è la lunghezza della stringa fa 3)., mentre PadRight allinea a destra inserendo 3 caratteri „*‟ come indicato.

Remove

Rimuove tutti i caratteri, o un certo numero prefissato,  da una stringa a partire da una certa posizione  Pertanto ci sono due formati:

Remove(posizione di partenza)
Remove(posizione di partenza, numero di elementi da togliere)

using System;
class program
{
    public static void Main()
    {
        string s1 = "abcdefgh";
        Console.WriteLine(s1.Remove(2));
        Console.WriteLine(s1.Remove(0, 2));
    }
}

L‟output è costituito dalle stringhe
ab
cdefgh

Come si può notare il comando non è distruttivo sulla stringa originaria. Per indicare posizione di partenza e numero di elementi da cancellare bisogna usare il tipo Int32.

Replace

Utile istruzione che sostituisce tutte le occorrenze di un carattere o di una stringa con un altro carattere o un'altra stringa.

using System;
class program
{    
public static void Main()
    {
        string s1 = "aaabbbccc";
        s1 = s1.Replace('a', 'b');
        Console.WriteLine(s1);
        Console.WriteLine(s1.Replace("bb","cc"));
        Console.ReadLine();
    }
}

La stringa finale sarà composta di sole "c‟.  

Split

Interessante e utile istruzione che permette di suddividere una stringa in una array di sottostringhe generate basandosi su di un separatore. Un primo semplice esempio è il seguente:

using System;
class program
{
    public static void Main()
    {
        string s1 = "cuori,quadri,fiori,picche";
        string[] a1 = s1.Split(',');
         foreach (string s in a1) Console.WriteLine(s);
        Console.ReadLine();
    }

Questo programma partendo dalla stringa s1 costruisce un array di 4 elementi ciascuno dei quali è una stringa, precisamente “cuori”, “quadri”, “fiori”, “picche”. L‟istruzione è in realtà molto duttile e su Internet si trovano vari esempi i utilizzo più avanzati. L‟esempio seguente fa un piccolo passo in più permettendo la separazione in parti di una stringa contenente più separatori diversi. Questi sono definiti tramite l‟array di caratteri a1.

using System;
class program
{
    public static void Main()
    {
        string s1 = "cuori,quadri;fiori picche";
        char[] a1 = { ' ', ':', ';', ',' };
        string[] a2 = s1.Split(a1);
        foreach (string s in a2) Console.WriteLine(s);
    }
}

StartsWith

Restituisce True se una stringa inizia con un certo carattere o una certa stringa.

using System;
class program
{
    public static void Main()
    {
        string s1 = "cuori,quadri,fiori,picche";
        Console.WriteLine(s1.StartsWith("cuori"));
        Console.ReadLine();
    }
}

SubString

Restiuisce una sottostringa a partire da un certo indice alla fine, come nel primo caso dell‟esempio che segue o a partire da un certo indice per un numero di posizioni predefinito (si usano sempre Int32).

using System;
class program
{
    public static void Main()
    {
        string s1 = "abcdefgh";
        Console.WriteLine(s1.Substring(2));
        Console.WriteLine(s1.Substring(2, 3));     
    }
}

L‟output è costituito dalle stringhe
cdefgh
cde

ToCharArray

Converte tutta la stringa in un array di caratteri Unicode. In alternativa converte in array di caratteri la porzione indicata a partire da un certo indice e per un numero definito di caratteri.

using System;
class program
{     public static void Main()
    {
        string s1 = "abcdefgh";
        char[] a1 = s1.ToCharArray();
        char[] a2 = s1.ToCharArray(2, 3);
        foreach (char c in a1) Console.Write(c + ":");
        Console.WriteLine();
        foreach (char c in a2) Console.Write(c + ":");
        Console.ReadLine();
    }
}

 L‟output è:

 

 

ToLower

Restituisce una copia della stringa con i caratteri convertiti tutti in minuscolo.

“aBcD”.ToLower() -> “abcd”

ToUpper

Restituisce una copia della stringa con i caratteri convetiti tutti in maiuscolo

“aBcD”.ToUpper() -> “ABCD”

Trim

Elimina all‟inizio e alla fine di una stringa i caratteri specificati. Senza nulla specificare elimina gli spazi.

using System;
class program
{
    public static void Main()
    {
        string s1 = "  aaaaa  ";
        Console.WriteLine(s1.Trim()+";");
        String s2 = "***aaaaa***";
        Console.WriteLine(s2.Trim('*')+";");
        Console.ReadLine();
    }
}

output:

aaaaa;
aaaaa;

È anche possibile specificare un array di caratteri da eliminare.

TrimStart e

Identici a Trim solo che agiscono rispettivamente solo all'inizio ed alla

TrimEnd

fine della stringa.


Questi metodi risolvono gran parte del lavoro che faremo sulle stringhe. Operativamente parlando posso dare alcuni suggerimenti pratici
  • La comparazione di due stringhe può essere fatta normalmente in maniera effciente e descrittiva tramite Equals oppure Compare o CompareTo. Anche in questo caso la cosa non è banale, occorre stare attenti alla cultura e ad altri particolari. C'è molto materiale in giro sulla rete.

  • La copia di una stringa in un‟altra è normalmente eseguita, sempre in maniera descrittiva, tramite Clone, oppure Copy o =. Per i nostri scopi sono tutti uguali.

  • Senza dubbio userete spesso l'operazione di la concatenazione di stringhe. La prima scelta è il + che viene internamente convertito in Concat, potete usare direttamente anche quello, anche se non c'è alcun guadagno pratico.  Si tratta di una soluzione abbastanza efficiente nella maggior parte dei casi che coinvolgono un ammontare limitato di stringhe. Il discorso relativo alla concatenazione non è semplice, se si va a guardare i dettagli ed i vari casi. Un profiler e l'esperienza vi potranno aiutare. Nel caso in cui sia richiesto un separatore non scordatevi di Join.

  • IndexOf è utile per verificare la presenza di una certa  sottostringa, anche costituita da un solo carattere. Anche Contains va bene, anche se è più generico restituendo una valore booleano.

  • Ricordatevi dell'esempio 12.1 per costruire con semplicità una stringa partendo da un array di caratteri.

le proprietà delle stringhe invece sono due:

Chars

Restituisce il carattere ad una data posizione, tramite l‟operatore [] che si usa in sua vece e che abbiamo già incontrato

Length

Restituisce il numero di caratteri che costituisce la stringa

“aaa”.Length

restituisce il numero 3. Questa proprietà come è facile capire è estremamente utile.

Chars, come detto, non è usata direttamente, si usano le parentesi quadre per selezionare un singolo elemento. Length invece l'abbiamo vista all'opera nell'esempio 12.2  ed è utile per navigare all'interno di una stringa prendendo in esame ogni suo singolo elemento. Tra le proprietà e i metodi di una stringa Length è probabilmente il più utillizzato in assoluto. Per selezionare i singoli elementi ovviamente va bene anche foreach, vi presento il seguente curioso esempio:

  Esempio 12.6
1
2
3
4
5
6
7
8
9
10
11
12
13
using System;
public class Test
{
  public static void Main()
  {
    string s1 = "1234";
    foreach (int i in s1)
    Console.WriteLine(i + " ");
    Console.WriteLine();
    foreach (char c in s1)
    Console.WriteLine(c + " ");
  }
}

che ci presenta:

49
50
51
52

1
2
3
44

La seconda sequenza, innescata dal foreach alla riga 10 e ci presenta i caratteri che compongono la stringa, anche i numeri sono caratteri al suo interno. La prima sequenza invece, creata tramite il foreach alla riga 7 ci fornisce gli interi rappresentanti, a livello di codice ASCII, i caratteri della stringa. Giusto una particolarità.

Un altro esempio che può essere utile, visto come ulteriore esempio applicativo dei metodi, è il seguente che vi permetterà di invertire una stringa senza fare uso dei soliti lenti loop che tanti usano per risolvere problematiche banali come questa:

  Esempio 12.7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;
using System.Text;
public class Class1
{
  public static void Main()
  {
    Console.Write("inserisci una stringa: ");
    string s1 = Console.ReadLine();
    char[] a1 = s1.ToCharArray();
    Array.Reverse(a1);
    s1 = new string(a1);
    Console.WriteLine("La stringa digitata rovesciata: " + s1);
  }
}

Il trucco sta nel fatto che gli array ammettono il metodo reverse. Una forma di collaborazione tra classi diverse, insomma.

Prima di passare oltre, ricordo che in questo paragrafo abbiamo appena sfiorato l'argomento delle "culture" ovvero i diversi alfabeti e fonemi presenti nei vari alfabeti sparsi per il mondo. E' un discorso lungo e complesso (e tedioso) che va però valutato se dovrete scrivere applicazioni di respiro "internazionale". In rete sono presenti molti articoli, alcuni di ottima qualità, che affrontano il problema.

STRINGBUILDER

La classe System.String è veramente utile e potente, lo abbiamo appena visto, ma presenta, come abbiamo accennato, peculiarità che la rendono poco pratica in caso di operazioni ripetute parecchie volte in particolare su stringhe di grandi dimensioni. Lo heap sarebbe pieno di relitti di stringhe dereferenziate ed esisterebbe il rischio di generare dei memory leak e con programmi grossi, dal punto di vista della computazione delle stringhe, questo potrebbe effettivamente capitare in maniera avvertibile. La loro immutabilità è un punto di forza ma anche di debolezza. Per ovviare a ciò .Net ci mette a disposizione il namespace System.Text all'interno del quale troviamo la classe StringBuilder. La particolarità degli oggetti creati a partire da questa classe è che essi possono essere modificati "in loco" quindi ogni variazione su essi non crea alcun nuovo elemento. Dal momento che la classe Stringbuilder vive all'interno di System.Text è necessario che quest'ultimo sia  importato all'inizio del programma. In generale comunque è meglio attenersi alla seguente regola: se prevedete che nel vostro programma vi siano molti accessi in modifica sulle stringhe usate Stringbuilder. In ogni altro caso usate le stringhe normali.

Per la dichiarazione è molto semplice:

StringBuilder sb1 = new StringBuilder("");

abbiamo dichiarato una StringBuilder si nome sb1 inizialmente vuoto (ma poteva non esserlo e in mezzo alla coppia di doppia apici potevate inserire la stringa che volevate). Per inserire ulteriori contenuti si fa uso del metodo Append(...).

sb1.Append("a");
sb1.Append("bb");

Lo StringBuilder alloca nello heap lo spazio necessario per tutti i caratteri che lo compongono, spazio che cresce ogni qual volta sia necessario. Se tuttavia lo spazio complessivo che vi servirà è noto a priori potete infilarlo direttamente nella definizione, un piccolo trick per migliorare le prestazioni:

StringBuilder sb1 = new StringBuilder("", 20);

In questo caso vengono preallocate venti poiszioni per accogliere i singoli elementi, ma se vi foste sbagliati e ve ne occorressero di più niente paura, il limite non è inviolabile e può essere ulteriormente ritoccato. Per sapere in anticipo quanti elementi potrebbe contenere al massimo lo StringBuilder abbiamo la proprietà MaxCapacity ma non vi sarà in genere particolarmente utile dato che riporta solo il dato relativo a Int32. Inserire il parametro numerico serve a preallocare quello spazio.
Una volta che abbiamo terminato la costruzione dello StringBuilder il metodo, immancabile, ToString() ci permette di riversarlo in una stringa.

 

  Esempio 12.8
1
2
3
4
5
6
7
8
9
10
using System;
using System.Text;
public class Class1
{
  public static void Main()
  {
    StringBuilder sb1 = new StringBuilder("");
    Console.WriteLine(sb1.MaxCapacity);
  }
}

Il risultato di questo programma è

2147483647

Il che ovviamente non vuol dire che lo spazio sia stato allocato ma che è allocabile ed è un dato che in genere, come abbiamo già detto, non vi servirà sapere ma può tornare comodo se doveste affrontare il problema del limite massimo utilizzabile su architetture diverse da quelle classiche x86, per esempio, anche se non se vi capiterà mai di lavorarci con .Net. La capienza reale dello StringBuilder che avete costruito è settabile e interrogabile tramite la proprietà Capacity. Potete utilizzarla per effettuare delle interrogazioni. Vediamo un esempio:

 

  Esempio 12.9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;
using System.Text;
public class Class1
{
  public static void Main()
  {
    StringBuilder sb1 = new StringBuilder("");
    Console.WriteLine(sb1.Capacity);
    sb1.Capacity = 15;
    Console.WriteLine(sb1.Capacity);
    sb1.Capacity = 8;
    Console.WriteLine(sb1.Capacity);
    sb1.Append("1234567890");
    Console.WriteLine(sb1.Capacity);
    sb1.Capacity = 7; // errore a runtime!!
  }
}

Ecco Capacity all'opera, anche come strumento per settare, a runtime, le dimensioni dello StringBuilder alle righe 8, 12 e 15. Quest'ultima riga provoca un errore a runtime in quanto sb1 contiene 8 caratteri e quindi la sua lunghezza non può essere ridotta sotto quelle dimensioni. Potete fare qualche prova per verificare come cresca lo spazio al crescere dei caratteri memorizzati. Scoprirete ad esempio che la capacità inizialmente impostata raddoppia se superare il valore che avete prefissato. Esiste un'altra proprietà interessante in questo senso ovvero Length. Questa, al contrario di quanto vista per le stringhe, non è solo interrogabile, fornendo il numero degli elementi presenti ma anche è in grado di dimnuirne il numero fino ad azzerarli. Naturalmente Length sarà di norma più piccolo della capacità. Vediamo l'esempio:

  Esempio 12.10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;
using System.Text;
class program
{
  public static void Main()
  {
    StringBuilder sb1 = new StringBuilder("abcdef");
    sb1.Capacity = 10;
    Console.WriteLine(sb1.Length);
    sb1.Length = 15;
    Console.WriteLine(sb1.Length);
    Console.WriteLine(sb1.Capacity);
  }
}

L'output è:

6
15
20

Il 6 è la lunghezza di sb1 dopo che vi è stata memorizzata la stringa "abcdef", composta di 6 caratteri. La capacità è 10. Portiamo la lunghezza a quota 15, la capacità  a sua volta viene estesa a 20. Se aggiungessimo l'istruzione

sb1.Length = 0;

la capacità rimarrebbe fissata a 20. Per capire ancora meglio cosa succedere possiamo ricorrere ancora all'operatore [].

 

  Esempio 12.11
1
2
3
4
5
6
7
8
9
10
11
12
13
using System;
using System.Text;
class program
{
  public static void Main()
  {
    StringBuilder sb1 = new StringBuilder("abcdef");
    Console.WriteLine(sb1[1]);
    sb1.Length = 2;
    Console.WriteLine(sb1.Capacity);
    Console.WriteLine(sb1[0]);
  }
}

che presenta questo output:

b
16
a

Tenete presente che anche Length non può ridurre la lunghezza ad una misura inferiore al numero di elementi presenti.
Visto che la cosa genera a volte un po' di confusione, è bene notare che Length predispone dello spazio effettivamente indicizzato, infatti l'operatore [] restituisce un carattere null se richiamato dopo essere stato indicato da Length, mentre Capacity indica una allocazione di memoria, una sorta di area predisposta ma assolutamente non richiamabile tramite []. Provate a mandare in esecuzione il seguente codice e vi renderete conto della cosa:

 StringBuilder sb1 = new StringBuilder("");
Console.WriteLine(sb1.Capacity);
Console.WriteLine((int)sb1[5]); //runtime error dovete eliminare questa riga per eseguire!
sb1.Length = 10;
Console.WriteLine((int)sb1[5]); //null - OK!

Vediamo invece all'opera la peraltro ovvia trasformazione di una vaiabile StringBuilder in stringa in questo altro frammento di codice:

StringBuilder sb1 = new StringBuilder("abcdef");
sb1.Append("gh");
string s1 = sb1.ToString();
Console.WriteLine(s1); 

Direi che non  presenta alcuna particolarità.

Vediamo ora metodi e proprietà principali di StringBuilder, anche in questo caso è una buona idea dargli un'occhiata approfondita.

Append

Come visto nin precedenza appende contenuto in coda a una istanza StringBuilder. E' certamente l'istruzione più usata in questo ambito.

AppendFormat

Piuttosto complesso, accoda una stringa formatta in base a zero o più specifiche istanze di formattazione. Ogni formato viene sostituito dalla rappresentazione in stringa di un oggetto che ha la funzione di argomento.

using System;
using System.Text;
using System.Globalization;
class program
{
     public static void Main()
     {
         StringBuilder sb1 = new StringBuilder("# ");
         StringBuilder sb2 = new StringBuilder("* ");
         float f = 3.333F;
         CultureInfo ci = new CultureInfo("de-DE", true);
         sb1.Append(f);
         sb2.AppendFormat(ci, f.ToString());
         Console.WriteLine(sb1.ToString());
        Console.WriteLine(sb2.ToString());
    }
}

 

AppendLine

Appende in coda la stringa e un fine riga.

using System; using System.Text;
class program
{
    public static void Main()
    {
        StringBuilder sb1 = new StringBuilder("aa");
        sb1.AppendLine("bb");
        Console.Write(sb1.ToString());
    }
}

Clear

Ripulisce l‟istanza a cui viene applicata.

sb1.Clear();

Per ripulire una istanza, è possibile ad esempio in maniera del tutto analoga negli esiti ricorrere alla proprietà Length, come visto in precedenza.

CopyTo

Copia una porzione in una array of char. Il format del commando è:

sb.CopyTo(posizione iniziale nella stringa, array di caratteri, indice iniziale nell’array, numero di caratteri))

using System;
using System.Text;
class program
{
    public static void Main()
    {
        StringBuilder sb1 = new StringBuilder("abcdef");
         char[] ar1 = {'*', '*', '*', '*'};
        sb1.CopyTo(1, ar1, 2, 2);
        foreach (char c in ar1) Console.WriteLine(c);
    }
}

EnsureCapacity

Garantisce che la capacità minima dello StringBuilder sia quella stabilita.

Equals

Stabilisce l‟uguaglianza tra due istanze

sb1.Equals(sb2);

Insert

Inserisce la rappresentazione in formato stringa di un certo oggetto nell‟istanza corrente. Il formato tipico è:

Insert(indice, oggetto);

using System;
using System.Text;
public class Class1
{
    public static void Main()
    {
        StringBuilder sb1 = new StringBuilder("aaaaaa");
         sb1.Insert(2, '2');
        Console.WriteLine(sb1.ToString());
    }
}  

Remove

Rimuove a partire da un certo indice x un numero n di elementi

sb1.Remove(x, n)

Replace

Rimpiazza tutte le occorrenze di un carattere o di una stringa. Quattro formati diversi:

sb1.Replace(char, char)
sb1.Replace(string, string)
sb1.Replace(char, char, int32, int32)
sb1.Replace(string, string, int32, int32)

I primi due sono chiari mentre come al solito I due interi indicano un indice di inizio in cui cercare il carattere o la stringa e quanto lungo deve essere l‟intervallo di ricerca.

using System;
using System.Text;
public class Class1
{
    public static void Main()
    {
        StringBuilder sb1 = new StringBuilder("aaaaaabbbbbbb");
        sb1.Replace('a', 'c', 2, 2);
        Console.WriteLine(sb1.ToString());
        Console.ReadLine();
    }
}

Infine sono utili 4 proprietà. peraltro già viste per lo più:

Capacity

Che ottiene o setta la capacità di una istanza StringBuilder

Chars

Che ottiene il carattere ad un certo indice

Length

Come visto restituisce o setta la lunghezza della istanza corrente

MaxCapacity

Restiuisce la massima capacità possibile.

Anche qui la proprietà Chars è mascherata tramite [], come per le stringhe.

E' evidente che la classe StringBuilder è disegnata allo scopo di compiere le operazioni elementari e più comuni sulle sequenze di caratteri in modo efficiente e rapido. La sua forza è proprio quella di svolgere egregiamente compiti ripetuti e massivi e la sua efficienza interna permetta a volte significativi risparmi di spazio in memoria. Un errore in cui però non bisogna cadere è quello di usare sempre e comunque questa classe. Va considerato infatti che la sua efficienza è frutto di una logica implementativa comunque abbastanza corposa che può risultare penalizzante in situazioni in cui non vi sia necessità di molte elaborazioni. Ovviamente anche a livello di codifica nuda e cruda bisogna valutare per bene la cosa. Tra l'altro la classe StringBuilder non ha la stessa quantità di membri di cui invece dispone la classe String (anche se, ad esempio, il Replace è migliore negli StringBuilder) e non è certamente una buona idea saltare da una all‟altra per applicare i metodi o le proprietà che ci servono... Qualcuno afferma anche che l‟uso di StringBuilder appesantisce la lettura del codice (può essere vero effettivamente... ma ognuno giudichi da sé). Per cui se dovete concatenare due stringhe o gestire un semplice input usate tranquillamente le classe String  nella sua forma naturale.

FORMATTAZIONE (Cenni)

E' un'altra questione abbastanza importante e utile. In breve C# permette di esprime l‟output in maniera che sia più facilmente leggibile e immediatamente comprensibile. L‟argomento è in realtà piuttosto complesso (nota personale: lo trovo anche un po' noioso...) e sarà ampliato a completato più avanti, quando saranno chiari altri concetti. 

In questa sezione darò solo qualche concetto introduttivo, più che altro per stimolare la curiosità sull‟argomento, nella sezione relativa gli esempi provvederò qualche dettaglio in più, mentre un po‟ di questo argomento sarà coperto in altri comparti, come quello relativo alla gestione delle date e delle ore .

Come abbiamo detto è possibile scrivere l‟output come segue;

Console.WriteLine(“x = {0}”, x);

Questa è una forma molto utile che può essere resa ancora più user friendly agli occhi dell‟utente in xaso di valori numerici attraverso opportune formattazione. Allo scopo esiste una tabella che ci aiuta in questo senso: 

c = currency, la valuta corrente

d = decimal

e = scientifico

f = fixed point

g = generale

n = con virgola

r = con arrotondamento

x = esadecimale

Ovviamente non tutte le varianti si possono applicare a tutti i numeri in alcuni casi possono essere sollevate delle eccezioni dovute ad un formato non corretto. Ad esempio il numero 2,1234 non potrà essere convertito in esadecimale. Analogamente la gestione della valuta corrente può dare qualche problema in videate di tipo DOS. Detto questo l‟esempio seguente può essere adattato e modificato a vostro piacere per compiere qualche prova:

  Esempio 12.12
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;
public class Class1
{
  public static void Main()
  {
    int i = 20; double d = 2.3456;
    Console.WriteLine("{0:c}", i);
    Console.WriteLine("{0:g}", i);
    Console.WriteLine("{0:d}", i);
    Console.WriteLine("{0:e}", i);
    Console.WriteLine("{0:f}", i);
    Console.WriteLine("{0:n}", i);
    Console.WriteLine("{0:x}", i);
    Console.WriteLine("{0:c}", d);
    Console.WriteLine("{0:g}", d);
    Console.WriteLine("{0:e}", d);
    Console.WriteLine("{0:f}", d);
    Console.WriteLine("{0:n}", d);
    Console.WriteLine("{0:r}", d);
  }
}

 

In generale il formato che viene usato segue la definizione

{“indice, [allineamento][:formato]”} 

E in questo senso possiamo scrivere cose del genere, utilizzando lo zero per la formattazione:

  Esempio 12.13
1
2
3
4
5
6
7
8
9
10
11
using System;

class Test
{
  public static void Main()
  {
    float value = 2.345f;
    Console.WriteLine("{0:00.000}", value);
    Console.WriteLine("{0:000.0000}", value);
  }
}

con questo output:

02,345
002,3450

Provate a vedere cosa accade aggiungendo una riga del tipo:

Console.WriteLine("{0:111.11111}", value);

Per utilizzare un placeholder generico si utilizza il simbolo #, quindi ad esempio:

Console.WriteLine("{0:##.####}", value);

Esistono poi possibilità di customizzare l‟esposizione delle date in molti formati diversi, cosa che vedremo quando parleremo della classe apposita di gestione di giorni, mesi, anni, ore ecc...