Zum Inhalt springen

Arbeiten mit .NET: Etwas Theorie/ Arrays

Aus Wikibooks

Was sind Arrays

[Bearbeiten]

Sinnhaftigkeit und Einsatz von Arrays sollen anhand eines kleinen Algorithmus, der aus einer Menge von Zahlen, die über die Tastatur nacheinander eingegeben werden,

  1. die Summe und
  2. den Maximalwert

bestimmen soll.

Prinzipiell stehen uns drei alternative Lösungsansätze zur Verfügung:

  1. Verwendung eines einfachen Speicherplatzes
  2. Verwendung mehrerer einfacher Speicherplätze
  3. Einsatz eines Arrays

1. Lösung: Einfacher Speicherplatz

Nach Eingabe eines Wertes erfolgt unmittelbar die Summierung und die Erhöhung der Anzahl der Eingabewerte um eins. Dabei wird zugleich geprüft, ob der zuletzt eingegebene Wert der Maximalwert ist. Das Ganze ließe sich wie folgt implementieren:

 
using System;

/* 
 * Kontext          Summation mehrerer Werte mit einfachen Variablen
*/
namespace array_01
{
    class Program
    {
        static void Main(string[] args)
        {
            int zahl, maximum, summe, anzahl;

            const string TITEL = "Zahlenauswertung mit einfachen Variablen [array_1]";
            Console.WriteLine(TITEL + "\n");
            Console.Title = TITEL;
            
            Console.Write("Zahl eingeben, Ende mit 0: ");
            zahl = Int32.Parse(Console.ReadLine());
            summe = zahl;
            anzahl = 0;
            maximum = zahl;

            while (zahl != 0)
            {
                anzahl++;
                Console.Write("Zahl eingeben, Ende mit 0: ");
                zahl = Int32.Parse(Console.ReadLine());
                if (zahl != 0)
                {
                    summe += zahl;
                    if (zahl > maximum)
                        maximum = zahl;
                }
            }
            Console.WriteLine("\nSumme aller Zahlen     : {0,3}", summe);
            Console.WriteLine("Anzahl der Zahlen      : {0,3}", anzahl);
            Console.WriteLine("Maximum der Zahlen     : {0,3}", maximum);
            Console.WriteLine("Mittelwert aller Zahlen: {0,6:f2}", (double)summe / anzahl);

            Console.WriteLine("\nProgrammende " + TITEL);
        }
    }
}

Dabei sind folgende Nachteile festzustellen:

  1. Alle zuvor eingegebene Werte werden überschrieben.
  2. Damit existieren keine weiteren Auswertungsmöglichkeiten (z.B. Abweichungen vom Mittelwert).

Will man also auf die zuvor eingegebenen Werte weiterhin zugreifen, muss verhindert werden, dass diese bei jeder erneuten Eingabe überschrieben werden. Dies zeigt der - bitte nicht ganz so ernst zu nehmende - 2. Lösungsansatz.

2. Lösung: Mehrere einfache Speicherplätze In dieser Lösung soll je Eingabe wird ein einfachen Speicherplatz verwenden werden. Ein wesentlicher Nachteil dieser Lösung ist natürlich, dass bei der Erstellung des Programms die Anzahl der einzugebenden Werte bereits feststeht muss, und diese auch zur Laufzeit nicht mehr änderbar ist. Ein Programm, das genau vier Werte entgegen nimmt und dann die Summe und den Mittelwert sowie die jeweilige Abweichung davon berechnet, könnte wie folgt aussehen:

 
using System;

/* 
 * Kontext          Summation mehrerer Werte mit mehreren Variablen
*/
namespace array_02
{
    class Program
    {
        static void Main(string[] args)
        {
            const int ANZAHL = 4;
            int zahl_1, zahl_2, zahl_3, zahl_4, maximum, summe;
            double mittelwert, abweichung_1, abweichung_2, abweichung_3, abweichung_4;
            const string TITEL = "Zahlenauswertung mit einfachen Variablen [array_2]";
            
            Console.WriteLine(TITEL + "\n");
            Console.Title = TITEL;

            // Eingaben
            Console.Write("1. Zahl: ");
            zahl_1 = Int32.Parse(Console.ReadLine());
            Console.Write("2. Zahl: ");
            zahl_2 = Int32.Parse(Console.ReadLine());
            Console.Write("3. Zahl: ");
            zahl_3 = Int32.Parse(Console.ReadLine());
            Console.Write("4. Zahl: ");
            zahl_4 = Int32.Parse(Console.ReadLine());

            // Auswertungen
            summe = zahl_1 + zahl_2 + zahl_3 + zahl_4;
            maximum = zahl_1;
            if (zahl_2 > maximum)
                maximum = zahl_2;
            if (zahl_3 > maximum)
                maximum = zahl_3;
            if (zahl_4 > maximum)
                maximum = zahl_4;

            mittelwert = (double)summe / ANZAHL;
            abweichung_1 = zahl_1 - mittelwert;
            abweichung_2 = zahl_2 - mittelwert;
            abweichung_3 = zahl_3 - mittelwert;
            abweichung_4 = zahl_4 - mittelwert;


            Console.WriteLine("\nSumme aller Zahlen     : {0,3}", summe);
            Console.WriteLine("Anzahl der Zahlen      : {0,3}", ANZAHL);
            Console.WriteLine("Maximum der Zahlen     : {0,3}", maximum);
            Console.WriteLine("Mittelwert aller Zahlen: {0,6:f2}", mittelwert);
            Console.WriteLine("Abweichung 1. Zahl     : {0,6:f2}", abweichung_1);
            Console.WriteLine("Abweichung 2. Zahl     : {0,6:f2}", abweichung_2);
            Console.WriteLine("Abweichung 3. Zahl     : {0,6:f2}", abweichung_3);
            Console.WriteLine("Abweichung 4. Zahl     : {0,6:f2}", abweichung_4);

            Console.WriteLine("\nProgrammende " + TITEL);
        }
    }
}


Unbestritten hat das Programm den Vorteil, dass alle Eingabewerte für weitere Auswertungen erhalten bleiben. Dem steht der oben genannte Nachteil gegenüber, dass die Anzahl der Eingabewerte bei Programmerstellung genau bekannt sein muss. Darüber hinaus finden sich in dem Programm viele identische Anweisungen. Dies liegt hier augenscheinlich an den unterschiedlich benötigten Variablenbezügen. Und schließlich stelle man sich vor, das Programm sollte für 1000 Zahlen eingesetzt werden!

Genau aus diesem Dilemma kann der Einsatz von Arrays herausführen.

C# und Arrays

[Bearbeiten]

Aus dem bisherigen Kurs sind Variable einfacher Datentypen bekannt. Sie stellen jeweils einen dem Datentyp entsprechend großen Speicherplatz unter einem Namen zur Verfügung. Demgegenüber können mittels strukturierter Daten/Datenstrukturen mehrere Speicherplätze unter einem Namen verwaltet werden. Sie fungieren also ähnlich wie ein Container für Daten.

Die Datenstruktur Array ist in C# als Klasse implementiert (System.Array), wobei ein Array eine Menge typidentischer Speicherplätze (Elemente) zur Verfügung stellt, und eine a-priori-Festlegung der Anzahl der Elemente im Rahmen der Deklaration erforderlich ist. Arrays unterscheiden die einzelnen Speicherplätze mittels des Variablennamens, ergänzt um einen Index, der in [] anzugeben ist.

Arbeiten mit Arrays

[Bearbeiten]

Zur Nutzung von Arrays sind die folgenden Schritte notwendig: Schritt 1: Deklaration der Referenzvariablen Schritt 2: Laufzeitobjekt durch Konstruktoraufruf erzeugen und Speicherplatz allokieren Schritt 3: schreibender/lesender Zugriff auf die Elemente

Schritt 1: Deklaration

[Bearbeiten]

Die Deklaration der Referenzvariablen erfolgt durch Angabe des Typs der Elemente des Arrays und durch Vergabe eines Namens. Im Unterschied zu einfachen Datentypen wird durch Angabe eines leeren Klammerpaars [] dieser Typ als Array gekennzeichnet. Dabei können alle einfachen Datentypen, aber auch strukturierte Daten selber als Grunddatentyp des Arrays verwendet werden.

Beispielhaft werden nachfolgend einige Referenzvariable für Arrays erstellt:

 
   int[] zahlen;
   bool[] wahrheit;
   string[] namen;
   double[] messwerte;
   char[] zeichensatz;

Schritt 2: Laufzeitobjekt erzeugen (Instanziierung)

[Bearbeiten]

Nach der Deklaration kann ein Laufzeitobjekt mittels Konstruktoraufruf new erzeugt werden, wobei jeweils die obere Grenze für das Array anzugeben ist. Die untere Grenze eines Arrays liegt per definitionem immer bei 0, wodurch sich eine Anzahl der Elemente aus obere Grenze - 1 ergibt. Der Konstruktoraufruf liefert den Verweis auf das Array, der mittels Zuweisung der Referenzvariablen zugewiesen wird:

 
   const int MAX = 1000;
   
   zahlen = new int[25];              // Elemente 0..24
   wahrheit = new bool[MAX];          // Elemente 0..MAX-1
   namen = new string[10];            // Elemente 0..9
   messwerte = new double[MAX * 10];  // Elemente 0..9999
   zeichensatz = new char[256];       // Elemente 0..255

Im Übrigen können Schritt 1 und 2 auch in einer gemeinsamen Deklaration stattfinden:

 
   const int MAX = 1000;
   
   int[] zahlen = new int[25];
   bool [] wahrheit = new bool[MAX];
   string[] namen = new string[10];
   double[] messwerte = new double[MAX * 10];
   char[] zeichensatz = new char[256];

Zu bemerken ist, dass nach dem Konstruktoraufruf alle Arrayelemente mit dem Defaultwert des jeweiligen Datentyps initialisiert sind.

Neben dieser Vorgehensweise gibt es weitere zur Deklaration, Erzeugung und Initialiserung von Arrays, die hier beispielhaft aufgezeigt werden:

 
   // Alternative Deklarationen
   // Kombination der Variablendeklaration, der Erzeugung und Belegung
      int [] zahlen = new int[3]  { 23, 12, 2 };
   // Kombination der Variablendeklaration, der Erzeugung und Belegung
      int [] zahlen = new int[]  { 23, 12, 2 };
   // Kombination der Variablendeklaration, der Erzeugung und Belegung
      int [] zahlen = { 23, 12, 2 };

Schritt 3: Zugriff auf die Elemente eines Arrays

[Bearbeiten]

Der Zugriff auf die einzelnen Elemente eines Arrays erfolgen nun über den Namen der Verweisvariablen und Angabe eines Indexwertes:

 
   zahlen[0] = 100;
   zahlen[1] = zahlen[0] * 2;
   wahrheit[999] = false;
   namen[5] = "Meier";
   messwerte[10] = Zahlen[1];
   zeichensatz[0] = 'm';

Erfolgt ein Zugriff außerhalb der Grenzen des Arrays, wird ein Laufzeitfehler erzeugt:

 
   zahlen[25] = 100;   // Laufzeitfehler, das Zahlenindex 0..24

Auf die einzelnen Elemente eines Arrays sind alle Operationen zulässig, die auch für den entsprechenden Grunddatentyp definiert sind: Ein-/Ausgabe von bzw. an Standardein-/-ausgabegeräte (Bildschirm, Tastatur), Zuweisung, relationale Operationen, arithmetische Operationen, Stringoperationen u.s.w.

Da die Arrayelemente mittels Index unterscheidbar sind, ihr Name aber immer identisch ist, bietet es sich an, bei Adressierung aller Elemente ein Schleifenkonstrukt zu verwenden. Hilfreich ist dabei, dass jedes Array über die Eigenschaft Length verfügt, die die Anzahl der Elemente enthält:

 
   for (int i = 0; i < zahlen.Length; i++)
      zahlen[i] = 3 * i;
   int summe = 0;
   for (int i = 0; i < zahlen.Length; i++)
     summe += zahlen[i];

Zusätzlich existiert in C# ein weiteres Schleifenkonstrukt, die foreach-Schleife, die alle Elemente eines Arrays automatisch iteriert. Zu beachten ist, dass innerhalb der Schleife allerdings nur ein lesender Zugriff auf die durch die Schleife adressierten Elemente möglich ist:

 
   int summe = 0;
   foreach (int z in zahlen)
     summe += z;

Beim Zugriff auf die gesamte Datenstruktur ist die Semantik der Zuweisung und des Vergleichs von Verweistypen zu beachten: Wird einer Referenzvariablen der Wert einer anderen Referenzvariablen zugewiesen, so adressieren beide anschließend den selben Speicherbereich!

Vorteile von Arrays

[Bearbeiten]
  1. Kurze prägnante Anweisungsfolgen durch Nutzung von Schleifenkonstrukten mit Laufindex als Arrayindex oder foreach-Schleifen
  2. Keine a priori Festlegung auf eine genaue Anzahl der zu verarbeitenden Daten (Ausnahme: maximale Länge des Arrays)).

Die hier genannten Vorteile fassen wir in einer neuen Version des o.g. Programms zusammen:

 
using System;
/* 
 * Kontext          Summation von genau max. 100 Werten
*/
namespace array_03
{
    class Program
    {
        static void Main(string[] args)
        {

            // Konstantendeklaration
            const string TITEL = "Zahlenauswertung mit strukturierten Variablen [array_03]";
            const int MAXANZAHL = 100;
            // Variablendeklaration
            int[] zahlen;
            double[] abweichungen;
            int maximum, summe, anzahl;
            double mittelwert;

            // Programmidentifikation
            Console.WriteLine(TITEL + "\n");
            Console.Title = TITEL;

            // Arrays allokieren
            zahlen = new int[MAXANZAHL];
            abweichungen = new double[MAXANZAHL];

            // Werte eingeben, solange Zahl != 0 und Länge array nicht überschritten

            anzahl = 0;
            do
            {
                Console.Write((anzahl + 1) + ". Zahl eingeben, Ende mit 0: ");
                //Zahlen[Anzahl] = Int32.Parse(Console.ReadLine());
                Int32.TryParse(Console.ReadLine(), out zahlen[anzahl]);
            }
            while ((zahlen[anzahl++] != 0) & (anzahl < zahlen.Length));
            anzahl--;

            // Berechnungen des Programms
            // Maximum berechnen
            maximum = zahlen[0];
            for (int i = 1; i < anzahl; i++)
                if (zahlen[i] > maximum)
                    maximum = zahlen[i];
            // Summe berechnen
            summe = 0;
            for (int i = 0; i < anzahl; i++)
                summe += zahlen[i];
            // Mittelwert berechnen
            mittelwert = (double)summe / anzahl;
            // Abweichungen berechnen
            for (int i = 0; i < anzahl; i++)
                abweichungen[i] = zahlen[i] - mittelwert;

            // Ausgaben
            Console.WriteLine("\nSumme aller Zahlen       : {0,3}", summe);
            Console.WriteLine("Anzahl der Zahlen        : {0,3}", anzahl);
            Console.WriteLine("Maximum der Zahlen       : {0,3}", maximum);
            Console.WriteLine("Mittelwert aller Zahlen  : {0,6:f2}", (double)summe / anzahl);
            for (int i = 0; i < anzahl; i++)
                Console.WriteLine("Abweichung Zahl {0} vom Mittelwert: {1,6:f2}", i + 1, abweichungen[i]);

            Console.WriteLine("\nProgrammende " + TITEL);
        }
    }
}

Dimensionen von Arrays

[Bearbeiten]

Ein-, zwei- und mehrdimensionale Arrays

[Bearbeiten]

In obigen Beispielen wurden jeweils Arrays, die über eine Dimension verfügen, verwendet. Es handelt sich also um eine Datenstruktur, die mit einem Vektor vergleichbar ist: Sie besitzt genau eine Dimension und stellt damit eine Zeile mit jeweils n Spalten oder eine Spalte mit jeweils n Zeilen zur Verfügung.

Arrays können daneben aber auch über beliebig viele Dimensionen verfügen. Bekannt ist bspw. aus der Mathematik die Matrix. Sie weist zwei Dimensionen auf, besteht sie doch aus mehreren Zeilen und Spalten. Eine entsprechende Datenstruktur in C# ist das zweidimensionale Array, für die entsprechende Referenzvariable wie folgt deklariert werden können:

 
   int[,] zahlenMatrix;
   bool[,] wahrheitMatrix;
   string[,] namenMatrix;
   double[,] messwerteMatrix;
   char[,] zeichensatzMatrix;

Innerhalb des Klammerpaars [] wird also eine Auflistung - getrennt durch jeweils ein Komma - aller Dimensionen vorgenommen, wobei, wie beim eindimensionalen Array, jeweils keine Angabe des Umfangs der Zeilen bzw. Spalten der Matrix erfolgt. Ebenso wie bei eindimensionalen Arrays erfolgt diese erst im Rahmen der Erzeugung (Instanziierung) der Datenstruktur:

 
   const int IMAX = 1000;
   const int JMAX = 200;
   
   zahlenMatrix[,] = new int[25, JMAX];        // Elemente 0,0..24,199
   wahrheitMatrix[,] = new bool[IMAX, JMAX];   // Elemente 0,0..IMAX-1, JMAX-1
   namenMatrix[,] = new string[10,2];          // Elemente 0,0..9,1
   messwerteMatrix[,] = new double[IMAX*10, 9];// Elemente 0,0..9999,8
   zeichensatzMatrix[,] = new char[256,3];     // Elemente 0,0..255,2

Die oben im Beispiel gezeigten Kombinationen der Deklaration, Instanziierung und Belegung mit Werten läßt sich gleichfalls für zwei- und selbstverständlich auch für mehrdimensionale Arrays verwenden:

 
      int[,] zahlenMatrix = new int[2,3] { 
                                            { 23, 42, 88 },
                                            { 17, -3,  5 }
                                          };
      
      int [,] zahlenMatrix = new int[,]  { 
                                            { 23, 42, 88 },
                                            { 17, -3,  5 }
                                         };

      int [,] zahlenMatrix =  { 
                                 { 23, 42, 88 },
                                 { 17, -3,  5 }
                              };

Nun dürfte klar sein, wie sich ein dreidimensionales Array deklarieren ließe:

 
   // Deklaration eines Schachbretts
   schachbretter[,] = new char[8,8];

   // Deklaration dreier Schachbretter
   schachbretter[,,] = new char[8,8,3];

Zugriff auf mehrdimensionale Arrays

[Bearbeiten]

Will man mehrdimensionale Arrays iterieren, wird für jede Dimension natürlich eine Schleife zu deren Iteration benötigt, damit alle Elemente des Arrays erreicht werden.Allerdings hilft hier die Eigenschaft Length des Arrays nicht weiter, da wir bei der Iteration ja die Länge jeder Dimension benötigen. Auf diese kann jedoch einfach über die Eigenschaft GetLength(int Dimension) zugeriffen werden. Mittels der Eigenschaft Rank eines Arrays kann auch die Anzahl seiner Dimensionen bestimmt werden:

 
   int[,] zahlenMatrix = { 
                            { 1, 2, 3 },
                            { 4, 5, 6 }
                         };
   
   Console.WriteLine("Ein {0}-dimensionales Array", zahlenMatrix.Rank);

   for (int i = 0; i < zahlenMatrix.GetLength(0); i++)
   {
       for (int j = 0; j < zahlenMatrix.GetLength(1); j++)
          Console.Write("{0,5}", zahlenMatrix[i, j]);
       Console.WriteLine();
   }

Arrays mit Referenztypen

[Bearbeiten]

In den obigen Beispielen wurden Arrays mit Elementen aus Werttypen verwendet. In gleicher Weise lassen sich in einem Array selbstverständlich auch Referenztypen (Verweistypen) speichern. Im Unterschied zu Arrays mit Werttypen muss innerhalb jeder Referenzvariablen innerhalb des Arrays jeweils die Referenz durch Konstruktoraufruf erzeugt werden.

Ein kleines Beispiel soll abschließend ein Array von Referenzen verdeutlich. Gehen wir dazu von einer stark vereinfachten Klasse Bruch aus, die aus zwei Instanzvariablen des Typs int (numerator = Zähler und denominator = Nenner) bestehen soll. Darüber hinaus gibt es nur zwei Methoden: Eine Methode zur zweilenweisen Ausgabe eines Bruchs im Format "numerator / denominator" und eine zweite Methode zur Addition eines Bruchs zu einem anderen Bruch.

Beispielhaft sei dies in folgender Klasse implementiert:

    
class Bruch
{
    // Instanzvariable Zähler und Nenner
    private int numerator;
    private int denominator;

    // Customkonstruktor
    // stellt sicher, dass der Nenner != 0 ist
    // erzeugt im Fehlerfall einen Bruch mit dem Wert 0 / 1
    public Bruch(int numerator, int denominator)
    {
        this.numerator = numerator;
        if (denominator == 0)
        {
            this.numerator = 0;
            this.denominator = 1;
        }
        else this.denominator = denominator;
    }

    // Instanzmethode print(): Gibt diesen Bruch in Form "Zähler / Nenner" 
    // auf der Konsole aus.
    public void print()
    {
        Console.WriteLine(numerator + " / " + denominator);
    }

    // Instanzmethode Addition eines Bruch zu dieser Bruchinstanz
    // Der Wert dieser Bruchinstanz wird geändert
    public void add(Bruch aBruch)
    {
        this.numerator = this.numerator * aBruch.denominator
                       + this.denominator * aBruch.numerator;
        this.denominator = this.denominator * aBruch.denominator;
    }
}

Exemplarisch soll nun ein Array für Brüche verwendet werden und mittels der Instanzmethode add die Summe aller Brücher ermittelt und ausgegeben werden. Die Erzeugung des notwendigen Speichers erfolgt bei Arrays von Referenztypen in zwei Schritten: 1. Erzeugung des Arrays 2. Erzeugung aller Brüche innerhalb des Arrays

Das Array brueche soll 10 Elemente lang sein, wobei jeder Bruch mit Zufallszahlen im Zähler und Nenner im Bereich 1 bis 10 ausgestattet sein soll, wobei exemplarisch für Zähler und Nenner mittels der Klasse Random Zufallszahlen verwendet werden sollen.

 
class Program
{
    static void Main(string[] args)
    {
        // 1. Brucharray mit Zufallsbrüchen erzeugen
        const int MAX = 10;
        Console.WriteLine("Erzeuge " + MAX + " Zufallsbrüche...");
        // Erzeugung des Brucharrays
        Bruch[] brueche = new Bruch[MAX];
        Random rnd = new Random();
        // Erzeugung der Brüche innerhalb des Brucharrays
        for (int i = 0; i < brueche.Length; i++)
            brueche[i] = new Bruch(rnd.Next(1, 11), rnd.Next(1, 11));

        // 2. Alle Brüche in der Form Zähler / Nenner ausgeben
        Console.WriteLine("\nAusgabe aller Zufallsbrüche...");
        foreach (Bruch bruch in brueche)
            bruch.print();
        // Alternativ
        //for (int i = 0; i < brueche.Length; i++)
        //    brueche[i].print();

        // 3. Die Summe aller Brüche bestimmen und ausgeben
        Console.WriteLine("\nSummiere alle Zufallsbrüche...");
        // Bruch mit dem numerischen Wert 0 erstellen
        Bruch summe = new Bruch(0, 1);
        // Alle Brüche im Array in summe addieren
        foreach (Bruch bruch in brueche)
            summe.add(bruch);
        // Alternativ
        //for (int i = 0; i < brueche.Length; i++)
        //     summe.add(brueche[i]);
        // Summe aller Brüche ausgeben
        Console.WriteLine("\nSumme aller Zufallsbrüche...");
        summe.print();
    }
}

}}