Lost in Translation: Die Folgen falsch formatierter Strings

Bob hat eine Aufgabe. Er muss 10 Würfel an zufälligen Positionen in einer Unity-Szene generieren und die Positionen in einer Textdatei speichern. In einer zweiten Unity-Szene sollen die Positionen aus der Datei gelesen und 10 Kugeln an den Positionen erstellt werden. Klingt einfach. Doch als Alice die Stellungen auf ihrem Computer lesen will, geht alles schief. Was ist passiert?

Disclaimer: Wenn Du CultureInfo bereits kennst und weißt, wie man es benutzt, wird dieser Artikel für Dich eher langweilig sein.

 

So erstellt Bob die Würfel und schreibt das Ergebnis in eine Datei:

using UnityEngine;
using System.IO;

public class WritePositions : MonoBehaviour
{
    string allPositions;
    Vector3 position;

    void Start()
    {
        Random.InitState(1);

        for (int i = 0; i < 10; i++)
        {
            // Würfel erzeugen
            var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);

            // Zufällige Position erzeugen
            position.x = Random.value * 10f;
            position.y = Random.value * 10f;
            position.z = Random.value * 10f;

            // Würfel auf zufällige Position setzen
            cube.transform.position = position;

            // xyz Werte der Position in Strings umwandeln
          // Semikolon als Trennzeichen hinzufügen
          // Strings zu einem längeren String verketten
            allPositions += position.x.ToString() + ";";
            allPositions += position.y.ToString() + ";";
            allPositions += position.z.ToString() + ";";
        }

        // Verketteten String in eine Textdatei schreiben
        string path = Application.dataPath + "/positions.txt";
        File.WriteAllText(path, allPositions);
    }
}

Das Ergebnis in seiner WritePositions-Szene sieht so aus:



Die Textdatei enthält nun alle xyz-Werte der Positionen. Das Semikolon ; wird als Trennzeichen genutzt:

9.996847;7.742628;6.809838;4.604562;5.944274;7.847895;9.143838;1.373541;2.568918;5.561134;7.822797;3.288125;4.600458;1.619433;9.611027;8.372383;8.775043;8.264214;2.13082;0.7605303;3.155688;6.640184;2.439498;3.610996;4.159934;3.951687;9.655395;7.099103;1.337068;2.075161;

Das gefällt Bob. Für den zweiten Teil seiner Aufgabe schreibt er diesen Code, um die Positionen auszulesen und die Kugeln zu erstellen:

using UnityEngine;
using System.IO;

public class ReadPositions : MonoBehaviour
{
    public bool savePositions = true;

    string allPositions;
    Vector3 position;

    void Start()
    {
        // Positionen aus dem Textfile in einen string einlesen
        string path = Application.dataPath + "/positions.txt";
        allPositions = File.ReadAllText(path);

        // Den String in ein Array aufsplitten
      // Das Semikolon wird als Trennzeichen genutzt
        string[] splitted = allPositions.Split(';');

        // Durch das Array gehen
        for (int i=0; i<splitted.Length-3; i+=3)
        {
            // Kugel erzeugen
            var sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);

            // Die Werte aus dem Array in Floats umwandeln
            float.TryParse(splitted[i], out position.x);
            float.TryParse(splitted[i+1], out position.y);
            float.TryParse(splitted[i+2], out position.z);

            // Die Kugel auf die umgewandelte Position setzen
            sphere.transform.position = position;
        }
    }
}

Das Ergebnis in seiner ReadPositions-Szene sieht so aus:

Aufgabe erledigt! Bob checkt seine Änderungen ein und macht Feierabend.


Alice möchte sich Bobs Lösung ansehen und startet Unity. Sie öffnet die ReadPositions-Szene und drückt auf Play:

Ähm... da ist nichts. Hat Bob überhaupt etwas getan? Warte, da sind Kugeln in der Hierarchie und es gibt eine Warnung im Inspektor:

Ok, die Position dieser Kugel scheint zufällig zu sein, aber sie ist wirklich groß. Alice ist sich sicher, dass Bob das nicht so gewollt hat. Sie beschließt, die Textdatei zu öffnen, um die Werte zu vergleichen:

9.996847;7.742628;6.809838;4.604562;5.944274;7.847895;9.143838;1.373541;2.568918;5.561134;7.822797;3.288125;4.600458;1.619433;9.611027;8.372383;8.775043;8.264214;2.13082;0.7605303;3.155688;6.640184;2.439498;3.610996;4.159934;3.951687;9.655395;7.099103;1.337068;2.075161;

Diese Werte machen Sinn. Aber sie merkt, dass der Dezimalpunkt verloren geht und deshalb die Werte in Unity so groß sind und die Kugeln außerhalb des Szene-Bereichs liegen. Aus 9.996847 wurde im Inspektor 9996847.

Dann fällt es Alice wie Schuppen von den Augen. Bob ist US-Amerikaner und sie ist Deutsche. Beide haben ihre Rechner auf verschiedene Sprachen eingestellt und das Zahlenformat in diesen Sprachen unterscheidet sich. Nehmen wir ein Beispiel:

USA:          1,234,567.89
Deutschland: 1.234.567,89

Das Fließkomma wird in den USA als . dargestellt, aber als , in Deutschland. Sie sind vertauscht. Alices Rechner interpretiert den Punkt nicht als Fließkomma, sondern als Tausendertrennzeichen. Der Code muss diesen Fall berücksichtigen.

Standardmäßig verwendet C# die Spracheinstellung des jeweiligen Rechners, wenn Zahlen in Zeichenfolgen konvertiert werden und umgekehrt. In Bobs Code geschieht dies in ToString() und TryParse(). Was hätte er anders machen sollen?


In C# gibt es die CultureInfo-Klasse. Sie befindet sich im System.Globalization-Namespace. Bob kann die InvariantCulture-Eigenschaft verwenden, um sicherzustellen, dass seine Konvertierungen auf verschiedenen Rechnern konsistent sind. Kleine Änderung, große Wirkung:

In WritePositions:

// System.Globalization namespace inkludieren
using System.Globalization;


// xyz Werte der Position in Strings umwandeln
// Semikolon als Trennzeichen hinzufügen
// Strings zu einem längeren String verketten
// Dabei die Konsistenz der Konvertierung sicherstellen

CultureInfo invariantCulture = CultureInfo.InvariantCulture;
allPositions += position.x.ToString(invariantCulture) + ";";
allPositions += position.y.ToString(invariantCulture) + ";";
allPositions += position.z.ToString(invariantCulture) + ";";

In ReadPositions:

// System.Globalization namespace inkludieren
using System.Globalization;

// Die Werte aus dem Array in Floats umwandeln

// Dabei die Konsistenz der Konvertierung sicherstellen
CultureInfo invariantCulture = CultureInfo.InvariantCulture;
NumberStyles style = NumberStyles.Float;

float.TryParse(splitted[i], style , invariantCulture, out position.x);
float.TryParse(splitted[i+1], style , invariantCulture, out position.y);
float.TryParse(splitted[i+2], style , invariantCulture, out position.z);

Mit dem NumberStyles Enum kann man verschiedene Stiloptionen kombinieren, z.B. Währungssymbole und Vorzeichen. NumberStyle.Float ist eine Kombination aus AllowLeadingWhite, AllowTrailingWhite, AllowLeadingSign, AllowDecimalPointund AllowExponent.


Jetzt funktioniert auch alles auf Alices Computer und Bob kann sich entspannen. Bei seiner Suche nach einer Lösung stieß er auf die C# Dokumentation und stellte fest, dass er damit ziemlich coole Sachen machen kann, wenn er Zahlen, Datumsangaben und andere Typen formatiert. Schau da unbedingt einmal rein:

Overview: How to format numbers, dates, enums, and other types in .NET

NumberStyles Enum

Über das beschriebene Problem bin ich in diversen Projekten gestolpert, insbesondere in Editor Tools. In teaminternen Tools können solche Fehler lange unentdeckt bleiben. Wenn Du jemals auf ein solch seltsames Verhalten stößt, wirf einen Blick auf den Code, der für die Konvertierung von Zeichenfolgen verantwortlich ist. Vielleicht geht ja irgendwo ein armer, unschuldiger Fließkommawert bei der Übersetzung verloren.

Zurück zur Startseite