Moderne Programmierung mit Swift, Teil 3
Seite 3: Zugriffsniveaus, Swift und Objective-C
Genau wie das Access Level Private regelt File-Private den Zugriff auf Eigenschaften und Funktionen innerhalb einer Quelldatei. Sind solche mit dem Schlüsselwort fileprivate versehen, lässt sich nur innerhalb der Quelldatei auf sie zugreifen, in der sie definiert sind. In anderen Dateien stehen sie nicht zur Verfügung.
Der nächste Quellcodeschnipsel unterscheidet mit Kommentaren zwischen zwei Source Files eines Projekts: der Datei A.swift und der Datei B.swift. In A.swift wird erneut eine StructureAStruct (ohne jegliche Eigenschaften und Funktionen) durch das Schlüsselwort fileprivate deklariert. Daraufhin erstellt das Programm in derselben Datei eine Instanz der Structure, was problemlos funktioniert. Der anschließende Versuch, innerhalb der Datei B.swift eine Instanz von AStruct zu erstellen, scheitert hingegen.
// Datei A.swift
fileprivate struct AStruct {}
fileprivate let myStruct = AStruct()
// Datei B.swift
fileprivate let anotherStruct = AStruct()
// Fehler: AStruct steht auĂźerhalb der Datei A.swift nicht zur VerfĂĽgung.
Das Access Level Internal ist der Standard in Swift und wird automatisch verwendet, sollte nicht explizit etwas anderes angegeben sein. Als Schlüsselwort kommt für die explizite Deklaration internal zum Einsatz. Internal weitet den Einsatz auf alle Quelldateien eines Modules aus. Typen, Eigenschaften und Funktionen mit diesem Access Level stehen so in jedem Source File zur Verfügung, das Teil desselben Modules ist. In anderen Modulen lässt sich aber nicht auf sie zugreifen.
Das folgende Listing simuliert erneut Source Files und zusätzlich zwei Module. Modul A enthält eine Datei namens A.swift, in der wieder eine Structure AStruct definiert wird. Sowohl in ihr als auch in der ebenfalls zum Modul gehörenden Datei B.swift werden Instanzen dieser Structure erstellt. Das ist möglich, da sich alles im selben Modul befindet.
In einem zusätzlich verfügbaren Modul B steht eine einzelne Datei C.swift bereit. Der Versuch, darin eine Instanz der Structure AStruct zu erstellen, endet in einem Compiler-Fehler.
// Module A
// Datei A.swift
internal struct AStruct {}
let firstStruct = AStruct()
// Datei B.swift
let secondStruct = AStruct()
// Module B
// Datei C.swift
let thirdStruct = AStruct()
// Fehler: AStruct steht auĂźerhalb des Modules A nicht zur VerfĂĽgung.
Public und Open
Public ist das erste Access Level, über das sich Freigaben über ein Modul hinaus definieren lassen. Mit dem zugehörigen Schlüsselwort public deklarierte Typen, Eigenschaften und Funktionen stehen auch außerhalb des Modules zur Verfügung, in dem sie definiert wurden. Solch ein Verhalten spielt beispielsweise bei der Entwicklung von Frameworks eine große Rolle.
Allerdings gilt es einige Einschränkungen im Zusammenspiel mit Klassen zu beachten: Von als public deklarierten Klassen lässt sich außerhalb des Moduls, innerhalb dessen sie definiert sind, keine Subklasse erstellen. Auch können keine als public gekennzeichneten Eigenschaften und Funktionen einer Klasse außerhalb ihres Ursprungsmoduls überschrieben werden.
Die genannten Beschränkungen des Access Level Public im Zusammenspiel mit Klassen hebt das letzte und offenste Access Level namens Open auf. Es entspricht in seiner Funktionsweise Public, erlaubt aber zusätzlich das Erstellen von Subklassen, selbst wenn die zugehörige Superklasse in einem anderen Module definiert ist. Mit dem Schlüsselwort open versehene Eigenschaften und Funktionen aus einem anderen Module können so an jeder beliebigen Stelle überschrieben werden.
Swift und Objective-C
Viele Projekte im Apple-Umfeld setzen heute – entweder ganz oder teilweise – noch auf Objective-C-Code. Um Entwicklern einen Umstieg auf Swift zu erleichtern, stellt Apple Techniken bereit, um Code in beiden Sprachen miteinander zu mischen, ohne dabei auf die Vorteile und Konzepte von Swift verzichten zu müssen. Einen Blick auf drei der spannendsten dieser Techniken liefern die folgenden Abschnitte. Los geht es mit einer Möglichkeit, das Prinzip der Optionals auf Objective-C zu übertragen.
Optionals gehören mit zu den spannendsten Sprachmerkmalen von Swift. Da ein vergleichbares Konzept in Objective-C fehlt, wird beim Einsatz von Objective-C-Code unter Swift davon ausgegangen, dass es sich bei in der Sprache deklarierten Properties um Implicitly Unwrapped Optionals handelt. Der folgende Code demonstriert das Standardverhalten und zeigt, wie sich Properties aus Objective-C in Swift importieren lassen.
// Objective-C
@property BOOL defaultProperty;
// Swift
var defaultProperty: Bool!
Auf die Weise können zwar Properties aus Objective-C unkompliziert in Swift verwendet werden, die Vorteile von Optionals in Swift gehen dadurch aber verloren. So werden beispielsweise Properties, die gar nicht als Optional ausgelegt sind und immer über einen Wert verfügen sollen, aus Objective-C nichtsdestoweniger als Implicitly Unwrapped Optionals importiert. Umgekehrt gibt es wiederum andere Properties, die besser als explizite Optionals aus Objective-C zu übernehmen sind.
Um das Verhalten zu optimieren, hat Apple Objective-C um drei neue Schlüsselwörter ergänzt, die sich als Option bei der Deklaration von Properties verwenden lassen. Sie können auch für den Rückgabetyp von Methoden und für Methodenparameter angewendet werden. Es handelt sich dabei um nonnull, nullable und null_unspecified.
Die erste Option, nonnull, gibt an, dass die entsprechende Property (oder der Methodenparameter oder -rückgabewert) niemals nil und damit kein Optional sein darf. nullable stellt das gegensätzliche Extrem dar: Die Option definiert, dass es sich beim entsprechenden Element um ein Optional handelt, das daher auch nil entsprechen kann.
null_unspecified stellt den Standard dar, der zum Einsatz kommt, wenn keine der anderen beiden Optionen gesetzt ist. Derartige Elemente werden als Implicitly Unwrapped Optionals in Swift importiert. Um zu verdeutlichen, wie die genannten Schlüsselwörter im Objective-C-Code zu setzen sind und wie sie sich auf den Import in Swift auswirken, zeigt der folgende Code unterschiedliche Beispiele. Darin werden sowohl Properties als auch Methodenparameter und -rückgabewerte mit den genannten Keywords versehen. Darunter folgt die importierte Deklaration in Swift.
// Objective-C
@property (nonnull) BOOL nonnullProperty;
@property (nullable) BOOL nullableProperty;
- (void)methodWithDefaultParameter:(BOOL)defaultParameter;
- (void)methodWithNonnullParameter:(nonnull BOOL)nonnullParameter;
- (void)methodWithNullableParameter:(nullable BOOL)nullableParameter;
- (BOOL)methodWithDefaultReturnValue;
- (nonnull BOOL)methodWithNonnullReturnValue;
- (nullable BOOL)methodWithNullableReturnValue;
// Swift
var nonnullProperty: Bool
var nullableProperty: Bool?
func methodWithDefaultParameter(_ defaultParameter: Bool!)
func methodWithNonnullParameter(_ nonnullParameter: Bool)
func methodWithNullableParameter(_ nullableParameter: Bool?)
func methodWithDefaultReturnValue() -> Bool!
func methodWithNonnullReturnValue() -> Bool
func methodWithNullableReturnValue() -> Bool?
Mithilfe der Schlüsselwörter schafft man es so, das Optional-Pattern auch aus Objective-C-Code heraus auf Swift zu übertragen. Zwar haben sie keine Auswirkungen auf die Programmierung in Objective-C, durch den angepassten Import in Swift lässt sich jedoch sicherstellen, dass wenigstens aus Swift heraus eine verbesserte und eindeutigere Nutzung der API möglich ist.
Generics in Objective-C
Swift ist eine typsichere Sprache. Elemente wie Arrays oder Dictionaries müssen exakt definieren, welche Art von Werten sie speichern und können nur mit Elementen eben jenes Typs umgehen. Erstellt man beispielsweise ein Array, das Strings enthält, ist es unmöglich, ihm andere Elemente – wie Integer – zuzuweisen. Objective-C ist weitaus weniger strikt. Standardmäßig kann beispielsweise ein Array auf Basis der Klasse NSArray munter Instanzen verschiedenster Typen mischen. Das wiederum erschwert aber den Import von Arrays aus Objective-C in Swift. Denn wenn in Objective-C die Angabe fehlt, für welche Typen ein Array konzipiert ist, muss Swift es so importieren, dass es mit jeder Art von Typ umgehen kann. Das funktioniert zwar, entspricht aber nicht der Typsicherheit von Swift.
Um dem Problem entgegenzuwirken, hat Apple an einigen Stellen seiner Frameworks Objective-C um die Funktion ergänzt, zusätzliche Typen als weitere Information mit angeben zu können. Dazu gehört beispielsweise auch die Klasse NSArray. Mit einer Swift-ähnlichen Syntax lässt sich einem NSArray in Objective-C ein fester Typ zuweisen. Damit wird ein solches Array in Swift auf die gewünschte Weise importiert und die Typsicherheit ist aus Objective-C-Code heraus gegeben:
// Objective-C
@property NSArray<NSString *> *strings;
// Swift
var strings: [String]
Selectors in Swift
Selectors spielen in Objective-C eine groĂźe Rolle, beispielsweise beim Target-Action-Pattern oder bei der Arbeit mit Notifications. Ein Selector stellt einen Verweis auf eine Methode dar, um sie dynamisch zu einem bestimmten Zeitpunkt oder beim AusfĂĽhren einer spezifischen Funktion aufzurufen.
Um den Ansatz in Swift nutzbar zu machen, hat Apple den Ausdruck #selector eingeführt. Ihm wird als Parameter jene Methode übergeben, die an einer entsprechenden Stelle als Selektor fungieren und aufgerufen werden soll. Die Methodennamen entsprechen den Swift-typischen Bezeichnungen. Wichtig dabei: In Swift können nur solche Methoden Selektoren erstellen, die mit dem @objc-Schlüsselwort deklariert sind.
Im Folgenden wird beispielhaft eine Klasse mit insgesamt drei Methoden deklariert, im Anschluss folgt das Erstellen von Selektoren fĂĽr jede der einzelnen Methoden.
class FirstClass {
@objc func firstMethod() {
// Do something...
}
class SecondClass {
@objc func secondMethodWithOneParameter(_ firstParam: Bool) {
// Do something...
}
}
@objc func thirdMethodWithTwoParameters(_ firstParam: Bool, secondParam: Bool) {
// Do something...
}
}
let firstSelector = #selector(FirstClass.firstMethod)
let secondSelector = ↲
#selector(FirstClass.SecondClass.secondMethodWithOneParameter(_:))
let thirdSelector = ↲
#selector(FirstClass.thirdMethodWithTwoParameters(_:secondParam:))
Intern entsprechen Instanzen, die mit #selector erzeugt werden, dem Typ Selector. Dieser lässt sich somit beispielsweise als Funktionsparameter verwenden.
Der große Vorteil beim Einsatz von #selector besteht darin, dass der Compiler feststellen kann, ob eine angegebene Methode tatsächlich existiert. In der ersten Version von Swift waren noch Strings zu verwenden, um derartige Selektoren umzusetzen, was sich als fehleranfällig herausstellte.