2016-04-20 69 views
35

Diciamo che ho una classe come questa:Elegante l'inizializzazione di un array di istanze di classi in C#

public class Fraction 
{ 
    int numerator; 
    int denominator; 

    public Fraction(int n, int d) 
    { 
     // set the member variables 
    } 

    // And then a bunch of other methods 
} 

voglio inizializzare un array di loro in modo piacevole, e questo post è una grande lista di approcci che sono inclini all'errore o sintatticamente ingombranti.

Naturalmente un costruttore di array sarebbe bello, ma non c'è nulla di simile:

public Fraction[](params int[] numbers) 

Quindi sono costretto a utilizzare un metodo come

public static Fraction[] CreateArray(params int[] numbers) 
{ 
    // Make an array and pull pairs of numbers for constructor calls 
} 

che è relativamente goffo, ma io non vedere un modo per aggirarlo.

Entrambe le forme sono soggette a errori perché un utente potrebbe erroneamente passare un numero dispari di parametri, forse perché ha saltato un valore, che lascerebbe la funzione grattarsi la testa chiedendosi cosa volesse effettivamente l'utente. Potrebbe generare un'eccezione, ma l'utente dovrebbe provare/catturare. Preferirei non imporlo all'utente, se possibile. Quindi applichiamo le coppie.

public static Fraction[] CreateArray(params int[2][] pairs) 

Ma non si può chiamare questo CreateArray in un modo bello, come

Fraction.CreateArray({0,1}, {1,2}, {1,3}, {1,7}, {1,42}); 

Non si può nemmeno fare

public static Fraction[] CreateArray(int[2][] pairs) 
// Then later... 
int[2][] = {{0,1}, {1,2}, {1,3}, {1,7}, {1,42}}; 
Fraction.CreateArray(numDenArray); 

Si noti che questo avrebbe funzionato bene in C++ (Sono abbastanza sicuro).

Sei costretto a fare uno dei seguenti invece, il che è aberrante. La sintassi è terribile e sembra davvero imbarazzante usare un array frastagliato quando tutti gli elementi hanno la stessa lunghezza.

int[2][] fracArray = {new int[2]{0,1}, /*etc*/); 
Fraction.CreateArray(fracArray); 
// OR 
Fraction.CreateArray(new int[2]{0,1}, /*etc*/); 

Allo stesso modo, le tuple in stile Python sono illegali e la versione C# è icky:

Fraction.CreateArray(new Tuple<int,int>(0,1), /*etc*/); 

L'uso di una matrice 2D puro potrebbe assumere il seguente modulo, ma è illegale, e sono che non ci sia modo legale per esprimerlo:

public static Fraction[] CreateArray(int[2,] twoByXArray) 
// Then later... 
Fraction[] fracArray = 
    Fraction.CreateArray(new int[2,4]{{0,1}, {1,2}, {1,3}, {1,6}}); 

Ciò non far rispettare le coppie:

public static Fraction[] CreateArray(int[,] twoByXArray) 

OK, come su

public static Fraction[] CreateArray(int[] numerators, int[] denominators) 

Ma poi i due array possono avere diverse lunghezze. C++ consente

public static Fraction[] CreateArray<int N>(int[N] numerators, int[N] denominators) 

ma, beh, questo non è C++, vero?

Questo genere di cose è illegale:

public static implicit operator Fraction[](params int[2][] pairs) 

e impraticabile in ogni caso, sempre a causa della sintassi aberrante:

Fraction[] fracArray = new Fraction[](new int[2]{0,1}, /*etc*/); 

Questo potrebbe essere bello:

public static implicit operator Fraction(string s) 
{ 
    // Parse the string into numerator and denominator with 
    // delimiter '/' 
} 

Poi si può fare

string[] fracStrings = new string[] {"0/1", /*etc*/}; 
Fraction[] fracArray = new Fraction[fracStrings.Length]; 
int index = 0; 
foreach (string fracString in fracStrings) { 
    fracArray[index] = fracStrings[index]; 
} 

Non mi piace questo approccio per cinque motivi. Uno, il cast implicito crea inevitabilmente un nuovo oggetto, ma ne abbiamo già uno perfetto, quello che stiamo cercando di inizializzare. Due, può essere fonte di confusione da leggere. Tre, ti costringe a fare esplicitamente ciò che volevo incapsulare in primo luogo. Quattro, lascia spazio alla cattiva formattazione. Cinque, si tratta di un'analisi univoca di stringhe letterali, che è più simile a uno scherzo pratico che a un buon stile di programmazione.

Quanto segue richiede anche uno spreco di istanze:

var fracArray = Array.ConvertAll(numDenArray, item => (Fraction)item); 

La seguente uso di una proprietà ha lo stesso problema a meno che non si utilizza quei terribili Jagged array:

public int[2] pair { 
    set { 
     numerator = value[0]; 
     denominator = value[1]; 
    } 
} 
// Then later... 
var fracStrings = new int[2,4] {{0,1}, /*etc*/}; 
var fracArray = new Fraction[fracStrings.Length]; 
int index = 0; 
foreach (int[2,] fracString in fracStrings) { 
    fracArray[index].pair = fracStrings[index]; 
} 

Questa variazione non impone paia :

foreach (int[,] fracString in fracStrings) { 
    fracArray[index].pair = fracStrings[index]; 
} 

Ancora una volta, questo approccio è comunque grande.

Queste sono tutte le idee che so come derivare. C'è una buona soluzione?

+1

C'è un [post correlati] (http://stackoverflow.com/questions/23534114/initialization-of-const-array-of-struct), che propone un trucco usando un elenco di oggetti. –

+0

@Axel - Bingo. – MackTuesday

risposta

38

Non riesco a pensare ad una soluzione elegante e allo stesso tempo efficiente per la memoria.

Ma c'è una soluzione elegante per la lista (e simili) utilizzando il C# 6 collection initializer caratteristica:

public static class Extensions 
{ 
    public static void Add(this ICollection<Fraction> target, int numerator, int denominator) 
    { 
     target.Add(new Fraction(numerator, denominator)); 
    } 
} 

Con questo metodo di estensione in atto, si può facilmente inizializzare una lista Fraction per esempio:

var list = new List<Fraction> { { 0, 1 }, { 1, 2 }, { 1, 3 }, { 1, 7 }, { 1, 42 } }; 

E, naturalmente, anche se non efficiente della memoria, è possibile utilizzarlo per inizializzare Fraction gamma sia:

var array = new List<Fraction> { { 0, 1 }, { 1, 2 }, { 1, 3 }, { 1, 7 }, { 1, 42 } }.ToArray(); 

o anche rendendo più concisa dichiarando un elenco classe derivata con implicito operatore di conversione array:

public class FractionList : List<Fraction> 
{ 
    public static implicit operator Fraction[](FractionList x) => x?.ToArray(); 
} 

e quindi utilizzare

Fraction[] array = new FractionList { { 0, 1 }, { 1, 2 }, { 1, 3 }, { 1, 7 }, { 1, 42 } }; 
+1

Mi piace per 'Lista <>'! –

+5

Ottima soluzione! E l'efficienza della memoria non dovrebbe preoccupare, se stiamo parlando di tuple scritte a mano nel codice! Se ce ne sono più di 1000 dovrebbero essere in un file di risorse! – Falco

+0

Richiede una determinata versione di C#? Ho provato il nuovo esempio {{0, 1}, {1, 2}, [...] 'ma dice che Add non richiede 2 argomenti, sebbene l'estensione sia in ambito. (Nota: non sono il richiedente) –

7

Il modo più succinto che posso pensare per il vostro esempio particolare comporta la scrittura di un operatore implicito per la classe Frazione:

public sealed class Fraction 
{ 
    public Fraction(int n, int d) 
    { 
     Numerator = n; 
     Deniminator = d; 
    } 

    public int Numerator { get; } 
    public int Deniminator { get; } 

    public static implicit operator Fraction(int[] data) 
    { 
     return new Fraction(data[0], data[1]); 
    } 
} 

Quindi è possibile inizializzare in questo modo:

var fractions = new Fraction[] 
{ 
    new [] {1, 2}, 
    new [] {3, 4}, 
    new [] {5, 6} 
}; 

Purtroppo si è ancora necessario il new [] su ogni riga, quindi non credo che questo guadagni molto rispetto alla normale sintassi di inizializzazione dell'array:

var fractions = new [] 
{ 
    new Fraction(1, 2), 
    new Fraction(3, 4), 
    new Fraction(5, 6) 
}; 

Immagino che si potrebbe scrivere un "locale" Func<> con un nome breve per semplificare l'inizializzazione un po ':

Func<int, int, Fraction> f = (x, y) => new Fraction(x, y); 

var fractions = new [] 
{ 
    f(1, 2), 
    f(3, 4), 
    f(5, 6) 
}; 

Lo svantaggio è che avresti bisogno di aggiungere che la linea in più (l'inizializzazione di un Func<>) ovunque tu voleva inizializzare l'array - o avere un metodo statico privato nella classe invece - ma allora quel metodo sarebbe all'interno della classe, il che non è l'ideale se ha un nome a una sola lettera.

Tuttavia, il vantaggio di questo approccio è che è molto flessibile.

Ho scherzato con l'idea di chiamare la funzione inline _, ma non ne sono davvero sicuro ...

Func<int, int, Fraction> _ = (x, y) => new Fraction(x, y); 

var fractions = new [] 
{ 
    _(1, 2), 
    _(3, 4), 
    _(5, 6) 
}; 
+0

Sì, la sintassi è migliore ma ancora goffa. Inoltre, direi che è soggetto a errori perché il parametro data potrebbe non avere due elementi. Nel peggiore dei casi, ha solo 0 o 1, il che genera un'eccezione e le potenziali eccezioni rendono più difficile l'implementazione di una tecnica. – MackTuesday

+2

Invece di un 'Func' locale, prenderei in considerazione la possibilità di renderlo un metodo' static statico' su alcune classi, alle quali è possibile accedere usando 'static'. – svick

+0

Grandi osservazioni, apprezzo tutte le tue intuizioni e spiegazioni, ho trovato il comico "underscore". –

8

Si potrebbe creare un costruttore di array di frazione con un'interfaccia fluida . Sarebbe portare a qualcosa di simile

public class FractionArrayBuilder 
{ 
    private readonly List<Fraction> _fractions = new List<Fraction>(); 

    public FractionArrayBuilder Add(int n, int d) 
    { 
    _fractions.Add(new Fraction(n, d)); 
    return this; 
    } 

    public Fraction[] Build() 
    { 
    return _fractions.ToArray(); 
    } 
} 

che può essere chiamato tramite

var fractionArray = new FractionArrayBuilder() 
    .Add(1,2) 
    .Add(3,4) 
    .Add(3,1) 
    .Build(); 

che è facile da capire dichiarazione.

Ho fatto uno fiddle per dimostrare.

+1

è possibile rilasciare il primo arg al metodo 'Add' e semplicemente restituire' this' –

+1

Questo non verrà compilato: se 'Add()' dovrebbe essere un metodo di estensione, deve essere 'static' . Ma non vedo alcuna ragione per cui non dovrebbe essere solo un metodo di istanza. – svick

+0

siete entrambi corretti. Ho modificato. –

Problemi correlati