Dart Spracheigenschaften

Bemerkung

In dieser Artikelreihe setze ich etwas Grundwissen in einer Programmiersprache mit C-ähnlicher Syntax (C/C++, Java, C#, Go, TypeScript) voraus und konzentriere mich vor allem auf die Besonderheiten von Dart. Es ist hilfreich sich als Einsteiger zuerst mit einer dieser Sprachen zu beschäftigen, denn Dart hat sonst eine recht steile Lernkurve.

Problemerfahrung mit ähnlichen Sprachen hilft beim Verständnis jener Konstrukte von Dart, die geschaffen wurden, um solche Probleme zu vermeiden. 🤓

Daten in Dart Programmen Link to heading

Die Auswahl an skalaren Datentypen ist bei Dart gegenüber anderen C-ähnlichen Sprachen begrenzt.

Es gibt nur drei numerische Datentypen:

int : Ganzzahlen mit max. 64 Bits Link to heading

Die genaue Bitlänge hängt von der Zielumgebung ab. Javascript als Zielumgebung etwa kennt nur 56 Bit lange Ganzzahlen. Die Dart VM (bei Ausführung ohne Transpilierung verwendet aber immer 64-Bit Zahlen).

Ansonsten gibt es auch keine kürzeren Zahlentypen mit 8, 16 oder 32 Bit und keine Unterscheidung von Zahlen mit oder ohne Vorzeichen (signed / unsigned).

Beispiel:

int x;          // Deklaration
int y = 100;    // Deklaration und Definition
z = 3           // Typinferenz (Dart bestimmt den Typ nach dem Wert)
red = 0xff000;  // Hexadezimalzahlen wie bei C/C++

double : Fließpunktzahlen mit 64 Bits Link to heading

Fließpunktzahlen entsprechen dem ISO Standard 754. Für Typinferenz muß ein Dezimalpunkt angegeben werden:

pi = 3.1415927;
plankLength = 1.616e-35; // Plancklänge in Meter

BigInt : beliebig genaue Ganzzahlen Link to heading

import 'dart:math';     // math library enthält Potenzierfunktion pow()

ten = BigInt.from(10);
gogol = ten.pow(100);   // die Zahl namens Gogol hat 101 Stellen

BigInts sind eigentlich kein integraler Bestandteil der Dart-Sprache, sondern nur wie eine Library - etwas umständlich - nutzbar. Es gibt also keine BigInt- Literale und nur die nötigsten überladene Operatoren.

Zeichenketten (strings) Link to heading

Es gibt auch keinen char-Typ wie in C/C++ sondern nur immutable Strings. Stringkonstanten können aber auch mehrzeilig sein. Als String Quotes können Apostrophe und Anführungszeichen verwendet werden. Strings können mit den Operatoren ‘+’ oder ‘,’ konkateniert werden, performanter geht das aber mit einer Stringbuffer-Klasse.

one = '1';           // es gibt keinen char-Typ
roman = '''Das Schweigen im Walde
Ein Roman in zwei Bänden von
"Ludwig Ganghofer"

Kapitel 1 ...''';
greeting = 'hello', ' world' + '!'; // Konkatenierung

Die Möglichkeiten zur Stringverarbeitung sind ansonsten mit Sprachen wie Python, Java oder JavaScript vergleichbar, auch reguläre Ausdrücke (regex) sind gut unterstützt.

Logic (bool Werte) Link to heading

Hinsichtlich logischer Werte ist mit der Klasse bool welche die Werte true und false kennt, alles wie bei anderen Sprachen der großen C-Familie, aber es gibt auch noch den Wert null, der seine eigene Klasse mit nur diesem Wert bildet.

Der Wert null Link to heading

Der Wert null steht für undefinierte (also nicht initialisierte) Variablen oder fehlende Ergebnisse.

Warnung

Eine Besonderheit des Dart-Compilers ist die Prüfung auf nicht initialisierte Variablen.

Bei deren Verwendung bricht die Übersetzung mit einem Fehler ab, denn der Wert null ist zu keiner anderen Klasse als Null kompatibel und eine nicht zugewiesene Variable hat immer den Wert null und ist somit ein Fehler. Deshalb sollte man Variablen stets initialisieren, wenn das möglich ist.

Andernfalls hängt man dem Typnamen ein Fragezeichen an, um temporär null-Werte zuzulassen. Das verpflichtet dann natürlich zu eigenen Prüfungen auf null.

Seit Dart 2.0 müssen Programme vollständig null safe sein. Ältere Code-Beispielen lassen sich deshalb manchmal nicht übersetzen.

Der dynamic Typ Link to heading

Eine Anleihe von C# ist der Datentyp dynamic. Solche Variablen haben keinen festgelegten Typ. Das ist vor allem nützlich bei Kollektionen, die aus einer nicht statisch typisierten Umgebung wie JavaScript, CSV-Datei oder Befehlszeilenparametern kommen. So können im Rahmen eines Parsers zum Datenimport und Übernahme in ein streng typisiertes Dart-Datenmodell erst mal in eine dynamische Liste eingelesen werden.

Als Beispiel ein Dart-Programm, das eine CSV-Datei importiert (dafür gibt es allerdings schon ein fertiges package csv im Dart-Repository, dieser simplifizierte Code dient nur der Demonstration).

import 'dart:io'; // zur Datei Ein/Ausgabe

// Ein/Ausgabe - Funktionen sind asynchron
void main() async {
  // CSV-Datei wird aus dem aktuellen Verzeichnis geladen
  final file = File('data.csv');
  // warten bis Datei geladen wurde
  final lines = await file.readAsLines();

  // Dynamische Liste zum Speichern der Daten
  final data = <List<dynamic>>[];

  // Jede Zeile in der CSV-Datei verarbeiten
  for (final line in lines) {
    // Zeile in Spalten zerlegen
    final columns = line.split(',');

    // Spalten in dynamische Liste konvertieren
    final row = <dynamic>[];
    for (final column in columns) {
      row.add(_parseColumn(column));
    }

    // Zeile zur Datenliste hinzufügen
    data.add(row);
  }

  // Daten ausgeben
  for (final row in data) {
    print(row);
  }
}

// Spalte in Liste einlesen
dynamic _parseColumn(String column) {
  // Versuchen, die Spalte als Zahl zu parsen
  final number = num.tryParse(column);
  // wenn parsen scheitert, ist's keine Zahl
  if (number != null) {
    return number; // Zahl eintragen
  }
  // String eintragen
  return column;
}

Auf TypeScript- oder C#- Programmierer dürfte der Code vertraut wirken, der gesamte Leistungsumfang wie auch der async-await Mechanismus zur Thread-Synchronisierung funktioniert bei Dart genauso bequem. Nur die vielen final Deklarationen geben Rätsel auf.

Konstanten und Speicherorte Link to heading

Ungewöhnlich bei Dart ist die Unterscheidung von final- und const- Konstanten.

final pi = 3.1415927     // dies ist eine Variable,
                         //deren Wert nicht mehr verändert werden darf

const yellow = 0xffff00; // der Compiler ersetzt **yellow** überall
                         // durch den Wert 0xffff00

Die häufige Verwendung von const zieht sich durch die gesamte Architektur von Dart und Flutter. Flutter-Widgets, die keine veränderlichen Werte enthalten, werden mit const-Klassen definiert. Das folgende Beispiel definiert eine simple Textbox mit einer überladenen build-Methode, welche die Textbox neu rendert, wenn sich der Inhalt ändert (den Code muss man im Detail hier noch nicht zu verstehen).

import 'package:flutter/material.dart';

// ein StatelessWidget ist **const** und kann keine
// darin dargestellten Werte mehr ändern und muss bei
// Änderungen neu gezeichnet werden.
//
// die dem Konstruktor beim Aufruf übergebenen Werte number und size müssen **final**
// sein, um innerhalb einer Instanz der Klasse keine Änderung mehr zuzulassen
//
class EntryLine extends StatelessWidget {
  // bei einem const Konstruktor darf die Instanz keine veränderlichen Werte besitzen
  const EntryLine({super.key, required this.number, required this.size});

  final String number; // angezeigter Text
  final Size size;     // Schriftgröße

  @override
  Widget build(BuildContext context) {
    // auch beim Aufbau des Widgets werden möglichst Konstanten
    // für alle enthaltene Elemente benutzt (siehe EdgeInsets und TextStyle)
    return Container(
      alignment: Alignment.centerRight,
      width: size.width,
      height: 50,
      padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
      decoration: BoxDecoration(
        color: Colors.teal[400],
        border: Border.all(color: Colors.black),
      ),
      child: Text(
        number,
        textAlign: TextAlign.end,
        style: const TextStyle(
            fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white),
      ),
    );
  }
}

Das Beispiel zeigt die funktionale Architektur von Dart. Es wird schon beim kompilieren sicher gestellt, das sich dieses Widget zur Laufzeit ändert, denn es wird bei neuen Werten auch neu gerendert. Wegen der const Konstanten kann der Dart-Compiler das Widget auch vorübersetzen, sodass die Laufzeit- Performance und der Speicherbedarf minimiert wird.

Das Letztere ist gerade für Mobilgeräte wichtig, denn vorübersetzter Code kann im nur lesbaren Speicher (flash rom) liegen. Nur veränderliche bzw finale Variablen liegen im oft knappen Hauptspeicher. Das führt zu einer reaktiven App mit geringerer Leistungsaufnahme.

C/C++ Entwickler können sich const-Daten als DATA Speichersegment vorstellen, BSS sollte gemieden werden und finals und variable Daten liegen auf dem HEAP.

Info
Fortsetzung folgt