In Crystal kann eine Variable aus mehreren Typen bestehen. Das nennt man einen Union-Typ. In Crystal ist es üblich, dass der Compiler einen Union-Typ selbst herleitet.
Ein Union-Typ ist zur Laufzeit trotzdem ein einzelner Typ, auch wenn er aus mehreren Typen besteht. Das heißt, wenn ein Union-Typ aus String und Int32 gebildet wird, ist er nicht beides gleichzeitig. Stattdessen ist er entweder ein String oder ein Int32.
Ein Union-Typ wird deklariert, indem man die Typen mit einem senkrechten Strich (|) trennt. Sie werden oft in runde Klammern gesetzt, nötig ist das aber nicht. Der häufigste Union-Typ ist (T | Nil), wobei T ein Typ ist. Er kommt in Methoden vor, die Nil zurückgeben können. Man sagt dann auch, die Variable ist nilable.
foo : String = "Hello"
foo = nil # Error: type must be String, not (String | Nil)
foo : (String | Nil) = "Hello"
foo = nil
Er ist nicht auf zwei Typen beschränkt, sondern kann so viele Typen umfassen, wie du willst.
foo : (String | Int32 | Nil | Float64) = "Hello"
foo = 1
foo = nil
foo = 1.0
typeof vs. Object#class
Es gibt zwei Möglichkeiten, den Typ einer Variable zu ermitteln. Entweder mit typeof oder mit Object#class. Der Unterschied ist, dass typeof den Typ einer Variable zur Kompilierzeit zurückgibt, während Object#class den Typ zur Laufzeit zurückgibt. Wenn du zum Beispiel herausfinden willst, ob eine Variable einen Union-Typ hat, kann Object#class dir das nicht sagen, denn es gibt nur den Typ zur Laufzeit zurück, und der ist ein einzelner Typ.
foo = 0 == 0 ? "a" : 1
typeof(foo) # => (String | Int32)
foo.class # => String
Da ein Union-Typ zur Laufzeit ein einzelner Typ ist, funktionieren alle Standardoperationen damit. Beim Kompilieren des Codes muss der Compiler aber wissen, um welchen Typ es sich handelt. Deshalb muss der Code so aufgebaut sein, dass er nur einer der Typen sein kann, wenn du typspezifische Operationen verwenden willst.
foo : (String | Int32) = "Hello"
foo.downcase # Error: undefined method 'downcase' for (String | Int32)
Crystal hat eine spezielle Methode für Union-Typen: die is_a?-Methode. Sie nimmt einen Typ als Argument und gibt einen booleschen Wert zurück. Die nil?-Methode ist eine Kurzform für is_a?(Nil). Wenn du die is_a?-Methode in einen Kontrollausdruck setzt, weiß der Compiler, um welchen Typ es sich handelt, und kann deshalb garantieren, dass es dieser Typ ist. Und für einen else-Zweig ist dann garantiert, dass es nicht dieser Typ ist.
foo : (String | Int32) = "Hello"
if foo.is_a?(String)
typeof(foo) # => String
foo.downcase # => "hello"
end
Dieses is_a? ist nicht auf einen einzelnen Typ als Argument beschränkt, sondern kann auch einen Union-Typ entgegennehmen. Es lässt sich außerdem mit && kombinieren, um mehrere Typen zu prüfen.
Die is_a?-Methode kann, wenn du sie zusammen mit einem Kontrollausdruck verwendest, keine Instanzvariable oder Klassenvariable sein. Stattdessen musst du sie zuerst einer lokalen Variable zuweisen.
Eine Möglichkeit, einen Union-Typ in einen einzelnen Typ umzuwandeln, besteht darin, einen Zweig nur dann zu betreten, wenn es sich um einen bestimmten Typ handelt. Ein anderer Ansatz ist die as-Methode. Sie wandelt einen Union-Typ mithilfe einer Prüfung zur Laufzeit in einen einzelnen Typ um. Ist der Typ nicht der erwartete, wird eine Exception ausgelöst.
foo : String | Int32 = "Hello"
foo.as(String).downcase # => "hello"
foo.as(Int32) # Error: can't cast String to Int32
Dieser Ansatz ist nur für den Fall gedacht, dass du sicher bist, dass der Typ der erwartete ist, oder dass du eine Exception auslösen willst, wenn er es nicht ist.
Verwendest du diesen Ansatz bei einem ungeeigneten Aufbau, kann das zu unerwartetem Verhalten führen.
as? funktioniert sehr ähnlich wie as, gibt aber nil zurück, statt eine Exception auszulösen, wenn der Typ nicht der erwartete ist. Das heißt, sie gibt einen Union-Typ aus dem erwarteten Typ und Nil zurück.
foo : (String | Int32) = "Hello"
foo.as?(String).downcase # => "hello"
foo.as?(Int32) # => nil
Nilable bedeutet, dass eine Variable entweder ein Typ oder Nil sein kann. Das kannst du als (T | Nil) schreiben. Da nilable Typen aber recht häufig vorkommen, gibt es dafür eine Kurzschreibweise: T?.
# This:
foo : (String | Nil) = "Hello"
foo = nil
# Is the same as:
foo : String? = "Hello"
foo = nil
Das Unternehmen, für das du arbeitest, steht kurz vor der Markteinführung seines brandneuen Smartphones Smarty 5. Es bietet ein brandneues Kamerasystem, ein neues Design und einen revolutionären neuen Prozessor, auf dem ein neues Betriebssystem namens smartyOS läuft.
Der neue Prozessor ist leistungsstark genug, um sicherere Passwörter zu verarbeiten als je zuvor, und das Unternehmen hat beschlossen, das zu seinem Vorteil zu nutzen. Es hat dich gebeten, ein Passwort-Sperrsystem für das Telefon zu implementieren, mit dem der Benutzer ein Passwort festlegen und dann prüfen kann, ob ein eingegebenes Passwort korrekt ist.
Das in dieser Übung verwendete Passwortsystem ist nicht sicher und dient nur zu Lernzwecken. Es sollte NICHT in echten Anwendungen verwendet werden.
Das Telefon erlaubt es Benutzern, verschiedene Arten von Passwörtern zu verwenden, von einem einfachen Ziffernpasswort über ein sichereres alphanumerisches Passwort bis hin zu einem Passwort, das ihren Fingerabdruck speichert.
Diese verschiedenen Passwortarten verwenden unterschiedliche Typen: Das Ziffernpasswort verwendet einen Int32, das alphanumerische Passwort einen String und das Fingerabdruck-Passwort einen Float64.
Implementiere die Klasse PasswordLock mit einem Initialisierer, der einen der drei Passworttypen als Argument entgegennimmt und ihn in einer Instanzvariable namens @password speichert.
password_lock = PasswordLock.new(1234)
# => #<PasswordLock:0x7f8e1b8c0b80 @password=1234>
Das Unternehmen hat beschlossen, das Passwort zu verschlüsseln, damit es nicht im Klartext gespeichert wird, und hat dich gebeten, eine Methode zum Verschlüsseln zu implementieren.
Jeder Passworttyp hat seine eigene Art der Verschlüsselung:
Int32-Passwort: Das Passwort wird durch zwei geteilt und auf die nächste Ganzzahl gerundet.String-Passwort: Das Passwort wird umgekehrt.Float64-Passwort: Das Passwort wird mit vier multipliziert.Implementiere eine Instanzmethode namens encrypt, die keine Argumente entgegennimmt und die Instanzvariable @password so verändert, dass sie verschlüsselt ist.
password_lock = PasswordLock.new(1234)
# => #<PasswordLock:0x7f8e1b8c0b80 @password=1234>
password_lock.encrypt
# => #<PasswordLock:0x7f8e1b8c0b80 @password=617>
Das Unternehmen hat dich außerdem gebeten, eine Methode zu implementieren, die prüft, ob ein eingegebenes Passwort korrekt ist.
Die Methode soll nil zurückgeben, wenn das Passwort falsch ist, und "Unlocked", wenn das Passwort korrekt ist.
Implementiere eine Instanzmethode namens unlock?, die das zu prüfende Passwort als Argument entgegennimmt.
Die Methode sollte nil zurückgeben, wenn das Passwort falsch ist, und "Unlocked", wenn das Passwort korrekt ist.
password_lock = PasswordLock.new(1234)
password_lock.encrypt
password_lock.unlock?(1234)
# => "Unlocked"
Melde dich bei Exercism an, um Crystal mit 26 Konzepte142 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.