Published by Arunprasadh C on 26 May 2022 • Last Updated on 02 June 2022
Classes in Swift
A Class is considered as a blueprint of objects. In Swift, a Class is similar to a Structure in most of the aspects. However, Swift Classes have the following unique properties and additional capabilities as compared to Structures:
- Classes are Reference Types unlike Structures. This means that the Objects of a Class are passed by Reference. Also, ARC allows multiple references of a class instance to exist.
- Classes support Inheritance.
- Classes support Typecasting.
- Classes can have Deinitializers (similar to C++ Destructors) in addition to Initializers.
Due to the above mentioned capabilities, Classes in Swift can leverage all the benefits of Object-oriented Programming. Classes in Swift are marked by the class
keyword.
Syntax :
class ClassName
{
//Class Properties and Methods
}
It is similar to struct
declaration, right ? So, when to choose Class over Structure ?
Choosing between Classes and Structures
The Apple Developer Documentation suggests the following Recommendations (Quoted from the Documentation):
-
Choose Structures by Default: Using structures makes it easier to reason about a portion of your code without needing to consider the whole state of your app. Because structures are value types—unlike classes—local changes to a structure aren’t visible to the rest of your app unless you intentionally communicate those changes as part of the flow of your app. As a result, you can look at a section of code and be more confident that changes to instances in that section will be made explicitly, rather than being made invisibly from a tangentially related function call. That is why even the Swift Standard Library uses Structures throughout and the usage of Classes is minimalized.
-
Use Classes When You Need Objective-C Interoperability: If you use an Objective-C API that needs to process your data, or you need to fit your data model into an existing class hierarchy defined in an Objective-C framework, you might need to use classes and class inheritance to model your data. For example, many Objective-C frameworks expose classes that you are expected to subclass.
-
Use Classes When You Need to Control Identity and Structures otherwise: Classes in Swift come with a built-in notion of identity because they’re reference types. This means that when two different class instances have the same value for each of their stored properties, they’re still considered to be different by the identity operator (
===
). It also means that when you share a class instance across your app, changes you make to that instance are visible to every part of your code that holds a reference to that instance. Use classes when you need your instances to have this kind of identity. Use structures when you’re modeling data that contains information about an entity with an identity that you don’t control. -
Use Structures and Protocols to Model Inheritance and Share Behavior: Structures and classes both support a form of inheritance. Structures and protocols can only adopt protocols; they can’t inherit from classes. However, the kinds of inheritance hierarchies you can build with class inheritance can be also modeled using protocol inheritance and structures. If you’re building an inheritance relationship from scratch, prefer protocol inheritance. Protocols permit classes, structures, and enumerations to participate in inheritance, while class inheritance is only compatible with other classes. When you’re choosing how to model your data, try building the hierarchy of data types using protocol inheritance first, then adopt those protocols in your structures.
Object Creation and Accessing Members
Object creation, accessing properties and methods of a class
and are all similar to struct
.
Example 1:
class Student
{
var rollNo: Int
var name: String
init(rollNo: Int, name: String)
{
self.rollNo = rollNo
self.name = name
}
}
let student = Student(rollNo: 1, name: "Kris")
print(student.rollNo)
print(student.name)
print(student)
Output 1:
1
Kris
helloworld.Student
You can notice that when I printed the student
object, I didn’t get a standard String
representation of the object like Structures. This is because class
in Swift doesn’t conform to CustomStringConvertible
Protocol by default unlike Structures and even this can be attributed to Inheritance because only at runtime, the type of object is decided.
Initializers
We have already seen about Initializers in Structures, but we will see more about them here. For stuctures, memberwise initializers are automatically generated by the compiler. But, for classes the compiler doesn’t generate Member-wise initializer on its own, as Classes allow Inheritance and it is difficult for the compiler to decide how the Initializer should be generated. If Initializer is not defined manually for the Class, the code won’t compile and the compiler will throw an error Class 'ClassName' has no initializers
.
However, if all the properties of a class or struct have default values, then a default initializer will be generated even if it is not manually written.
The Initializers for Classes are classified into various types. The general Initializers are called Designated Initializers while the Initializers which make the calling of Designated ones easier, are called as Convenience Initializers and are marked by the keyword convenience
. Convenience Initializers are generally used for assigning default values to properties. Designated and Convenience Initializers will be explored in detail while exploring Inheritance. We can also define Failable Initializers which can fail and return nil
instead of an instance. Failable Initializers are defined by marking the init
keyword with Question Mark ?
, like init?
. Failable Initializers will always return Optional
wrapped Instances which have to be unwrapped for further use. Even !
can be used with Failable initializer like init!
to throw an error when Initialization fails. When a property needs to be assigned the same initial value all the time, it is advised to give the default value in the property declaration itself, rather than assigning the default value via an initializer.
Example 2:
class Age: CustomStringConvertible
{
var description: String
{
"Age(\(value))"
}
var value: Int
init?(_ value: Int)
{
if value < 0 || value > 100
{
print("Invalid Age \(value)! ")
return nil
}
else
{
self.value = value
}
}
init()
{
self.value = Int.random(in: 0...100)
}
}
for x in [-2, 21, 500]
{
if let age = Age(x)
{
print(age)
}
else
{
print("As Invalid Age \(x) is provided, randomizing Age Value: \(Age())")
}
}
Output 2:
Invalid Age -2!
As Invalid Age -2 is provided, randomizing Age Value: Age(3)
Age(21)
Invalid Age 500!
As Invalid Age 500 is provided, randomizing Age Value: Age(82)
Deinitializer
In addition to Initializers, Swift Classes can also have a single Deinitializer, which is called just before the Object of the Class is deallocated (Similar to C++ Destructors). These can be used for performing additional tasks like closing a file or any held resource, thereby freeing up the memory before the class object itself is deallocated. Messages printed inside Deinitializer block can generally be realized when the object is being deallocated.
Example 3:
class Student: CustomStringConvertible
{
var rollNo: Int
var name: String
var description: String
{
"Student(rollNo = \(rollNo), name = \(name))"
}
init(rollNo: Int, name: String)
{
self.rollNo = rollNo
self.name = name
}
func changeRollNoAndName(newRollNo:Int, newName: String)
{
rollNo = newRollNo
name = newName
}
deinit
{
print("Student: \(name) is being deinitialized...")
}
}
var student: Student? = Student(rollNo: 1, name: "Kris")
print(student!)
student = nil // Deinitializing the object
let _ = Student(rollNo: 2, name: "Siva") // Using '_' to indicate ignored or unused object. Hence it is deallocated immediately.
Output 3:
Student(rollNo = 1, name = Kris)
Student: Kris is being deinitialized...
Student: Siva is being deinitialized...
When nil
is assigned to the Optional student
object, it means that it can be deallocated and hence the deinitializer is called. Underscore _
is used for Wildcard Pattern Matching in Swift. It is used whenever a variable or constant is ignored or unwanted. It is also used when the return type of a function is ignored. It is used in for loops too when the iterating variable can be ignored. In the Above example, the _
indicates that the object can be ignored or is not going to be used. Hence, the scope of the object gets over as soon as it is created and hence, the deinitializer is called.
Protocol Conformance
Classes can also conform to Protocols like Structures and use the same syntax as Structures for conforming. We will see more about Protocols in a separate Topic Page.
Behaviour with let
Constants
As mentioned earlier in Structures, let
Constants behave differently with Classes. The regular behaviour of preventing reassigning of objects is maintained. However, the properties of Class object can be mutated even when using let
, as Classes are Reference Types.
Example 4:
class Student: CustomStringConvertible
{
var rollNo: Int
var name: String
var description: String
{
"Student(rollNo = \(rollNo), name = \(name))"
}
init(rollNo: Int, name: String)
{
self.rollNo = rollNo
self.name = name
}
}
let student = Student(rollNo: 1, name: "Kris")
print(student.description)
print(student.rollNo)
student.name = "Shiv" // Modifying the property of a let constant object
print(student.name)
print(student)
Output 4:
Student(rollNo = 1, name = Kris)
1
Shiv
Student(rollNo = 1, name = Shiv)
You can see that the name was changed even though student
is a let
constant.
No need of mutating
Keyword for Mutating Methods
Unlike struct
, the Mutating Methods of class
don’t need the mutating
keyword, as Classes are Reference Types.
Example 5:
class Student: CustomStringConvertible
{
var rollNo: Int
var name: String
var description: String
{
"Student(rollNo = \(rollNo), name = \(name))"
}
init(rollNo: Int, name: String)
{
self.rollNo = rollNo
self.name = name
}
func changeRollNoAndName(newRollNo:Int, newName: String)
{
rollNo = newRollNo
name = newName
}
}
let student = Student(rollNo: 1, name: "Kris")
print("Original Value: \(student)")
student.changeRollNoAndName(newRollNo: 2, newName: "Shiv")
print("After calling changeRollNoAndName() Method: \(student)")
Output 5:
Original Value: Student(rollNo = 1, name = Kris)
After calling changeRollNoAndName() Method: Student(rollNo = 2, name = Shiv)
Equality and Identity Logic
Unlike Structures, the Identity Operators (===
and !==
) can be used with Classes. Identity Operators compare the memory uniqueness of two class instances. They don’t check if the values are equal. They instead check if the two instances point to the same reference in the memory. In order for Equality Operators (==
and !=
) to work, the Class has to conform to the Equatable
Protocol. However, unlike Structures, it is not optional to override the ==
function. It must be redefined inside the class for the equality operators to work. The equality operators don’t care about the memory address. They rather care about the properties of the instances.
Example 6:
class Student: CustomStringConvertible, Equatable
{
var rollNo: Int
var name: String
var description: String
{
"Student(rollNo = \(rollNo), name = \(name))"
}
init(rollNo: Int, name: String)
{
self.rollNo = rollNo
self.name = name
}
func changeRollNoAndName(newRollNo:Int, newName: String)
{
rollNo = newRollNo
name = newName
}
static func == (lhs: Student, rhs: Student) -> Bool
{
lhs.name == rhs.name && lhs.rollNo == lhs.rollNo
}
deinit
{
print("Student: \(name) is being deinitialized...")
}
}
let student1 = Student(rollNo: 1, name: "Kris")
let student2 = student1
let student3 = Student(rollNo: 2, name: "Kris")
print("Student 1: \(student1)")
print("Student 2: \(student2)")
print("Student 3: \(student3)")
print("student1 === student2: \(student1 === student2)")
student2.rollNo = 2 // Changing rollNo of student 2
print("Student 1: \(student1)") // Student 1 rollNo also changed. Call by reference behaviour
print("Student 2: \(student2)")
print("student2 === student3: \(student2 === student3)") // Even though Same value but different reference, so false
print("student2 == student3: \(student2 == student3)") // Even though different reference, same value is present. so == returns true
print("student1 == student2: \(student1 == student2)")
print("student1 == student3: \(student1 == student3)")
let student4 = Student(rollNo: 3, name: "Ram")
print("Student 4: \(student4)")
print("student4 != student1: \(student4 != student1)")// true as rollNo not and Name equal
Output 6:
Student 1: Student(rollNo = 1, name = Kris)
Student 2: Student(rollNo = 1, name = Kris)
Student 3: Student(rollNo = 2, name = Kris)
student1 === student2: true
Student 1: Student(rollNo = 2, name = Kris)
Student 2: Student(rollNo = 2, name = Kris)
student2 === student3: false
student2 == student3: true
student1 == student2: true
student1 == student3: true
Student 4: Student(rollNo = 3, name = Ram)
student4 != student1: true
Inheritance using Classes
Unlike Structures, Classes can inherit properties and methods from one another. The Class which inherits from another class is called as Subclass/Child Class and the Class whose members are inherited is called the Super Class/Parent Class(It is also known as Base Class, if it doesn’t inherit from other classes). Classes in Swift can call and access methods, properties, and subscripts belonging to their superclass and can provide their own overriding versions of those methods, properties, and subscripts to refine or modify their behavior. Swift helps to ensure your overrides are correct by checking that the override definition has a matching superclass definition. Classes can also add property observers to inherited properties in order to be notified when the value of a property changes. Property observers can be added to any property, regardless of whether it was originally defined as a stored or computed property.
Is-A Relationship
Inheritance should be used only when there exists an Is-A Relationship between Two Classes. For example:
- Car is a Vehicle.
- Student is a Person.
- Dog is an Animal. Here, Car can inherit members from Vehicle, Student from Person and so on.
The Syntax of Inheritance is as follows: Syntax :
class Subclass : Superclass
{
//Class Members
}
The Colon (:
) Symbol is used to indicate Inheritance (In terms of Swift, we can even compare it with Type Annotations. We can infer that the SubClass is of Superclass Type
). The override
keyword is used for overriding the Non-private
instance methods, type methods, instance properties, type properties, initializers and subscript of the super class. The super
keyword is used for accessing the Non-private
members of the super class from the subclass. Always, the overridden members are given priority when called from the child classes. A simple example is given below:
Example 7:
class Animal: CustomStringConvertible
{
var description: String
{
"Animal(isMammal: \(isMammal))"
}
var isMammal: Bool
init(isMammal: Bool)
{
self.isMammal = isMammal
}
func makeSound()
{
fatalError("This method must be overridden by subclasses for it to work!!!") // Abstract Methods can be implemented like this
}
}
class Dog: Animal
{
override var description: String
{
"Dog(isMammal: \(isMammal), isStrayDog: \(isStrayDog))"
}
var isStrayDog: Bool
override func makeSound()
{
print("Woof!!!")
}
init(isStrayDog: Bool)
{
self.isStrayDog = isStrayDog
super.init(isMammal: true)
}
}
var animal: Animal = Animal(isMammal: false)
print(animal)
print(type(of: animal))
animal = Dog(isStrayDog: false) // Works since Dog is a subclass of Animal.
animal.makeSound()
print(animal)
print(type(of: animal))
Output 7:
Animal(isMammal: false)
Animal
Woof!!!
Dog(isMammal: true, isStrayDog: false)
Dog
In the above example, Animal
is the base class from which subclass Dog
is derived. Animal
has a method makeSound()
which becomes valid only when it is overridden by subclasses. Conceptually, makeSound()
is meant to be abstract. However, Swift doesn’t support abstract methods inside classes. So, that behaviour can be somewhat mimicked by throwing an error when the method is called from an Animal
instance. But, this is not the right way to achieve abstract methods. Swift provides Protocols for the purpose. We will see about Protocols later. Note that, the super.init
call of Dog
class is done after initializing Dog
’s properties only (Unlike Java, where super()
is called first most of the times). This is done so to ensure that the Dog
’s properties are initialized first, so that any overridden method can access them without any issues. As you can see in the example, a variable of type Animal
(Base class) is allowed to hold either instance of itself or an instance of its subclass. Even reassigning to subclass reference is accepted. However, the vice versa is not true because a subclass may define additional properties and methods. Moreover, when a base class variable is assigned with subclass reference, only methods and properties available commonly in both subclass and superclass can be accessed. To access other members, downcasting must be done (It will be discussed in detail in Type Casting Subtopic).
Types of Inheritance supported by Swift
Swift supports the following Inheritance Types:
- Single Inheritance
- Multilevel Inheritance
- Hierarchical Inheritance
- Hybrid Inheritance
Like Java, Swift too doesn’t support Multiple Inheritance due to the Diamond Problem, which causes ambiguity in which overridden method should be called from grandchild class instances. Instead, it can be mimicked by use of Protocol Conformance.
Overriding Methods
Methods of super class can be overridden by subclass to provide new implementations to the methods. The only restriction is that the overriding methods must have an equal or lower restrictive access level than the super class method. Else, the compiler will throw an error. For example, an internal
method can be overridden as public
or open
but not as private
or fileprivate
method. An instance of Method overriding is shown above in Example 7, where the makeSound()
Method is overridden in the subclass.
Overriding Property Getters and Setters and Property Observers
You can provide a custom getter (and setter, if appropriate) to override any inherited property, regardless of whether the inherited property is implemented as a stored or computed property at source. The stored or computed nature of an inherited property isn’t known by a subclass—it only knows that the inherited property has a certain name and type. You must always state both the name and the type of the property you are overriding, to enable the compiler to check that your override matches a superclass property with the same name and type. You can present an inherited read-only property as a read-write property by providing both a getter and a setter in your subclass property override. You can’t, however, present an inherited read-write property as a read-only property.
NOTE : If you provide a setter as part of a property override, you must also provide a getter for that override. If you don’t want to modify the inherited property’s value within the overriding getter, you can simply pass through the inherited value by returning super.someProperty from the getter, where someProperty is the name of the property you are overriding.
In the Example 7 above, the description
Property is overridden in the Dog
class. Even Property Observers like didSet
or willSet
can be overridden in the subclass. But, both custom setters and property observers can’t be overridden simultaneously.
Static Methods vs Class Methods
Swift allows to define class func
methods, which are available at Class Level, rather than at Instance level. It is available exclusively for Classes in Swift and and cannot be used by Structures and Enumerations. So, what is the difference between static func
and class func
? class func
uses Dynamic Dispatch and hence supports Overriding. However, static func
can’t be overridden in subclass. static func
is equivalent to final class func
as the final
keyword prevents the method from getting overridden.
Example 8:
class A
{
static func nonOverridableFunc() // Equivalent of final class func
{
print("Non-overridable static function")
}
class func overridableFunc()
{
print("Overridable class function (Now in class A)")
}
final class func nonOverridableFunc2()
{
print("Non-overridable class function mimicking static function")
}
}
class B: A
{
override class func overridableFunc()
{
print("Overridable class function (Now in class B)")
}
}
A.nonOverridableFunc()
A.overridableFunc()
B.overridableFunc()
A.nonOverridableFunc2()
Output 8:
Non-overridable static function
Overridable class function (Now in class A)
Overridable class function (Now in class B)
Non-overridable class function mimicking static function
Designated and Convenience Initializers
Designated initializers are the primary initializers for a class. A designated initializer fully initializes all properties introduced by that class and calls an appropriate superclass initializer to continue the initialization process up the superclass chain.
Classes tend to have very few designated initializers, and it’s quite common for a class to have only one. Designated initializers are “funnel” points through which initialization takes place, and through which the initialization process continues up the superclass chain.
Every class must have at least one designated initializer. In some cases, this requirement is satisfied by inheriting one or more designated initializers from a superclass, as described in Automatic Initializer Inheritance below.
Convenience initializers are secondary, supporting initializers for a class. You can define a convenience initializer to call a designated initializer from the same class as the convenience initializer with some of the designated initializer’s parameters set to default values. You can also define a convenience initializer to create an instance of that class for a specific use case or input value type. Convenience Initializers are denoted by attaching the keyword convenience
before init
.
You don’t have to provide convenience initializers if your class doesn’t require them. Create convenience initializers whenever a shortcut to a common initialization pattern will save time or make initialization of the class clearer in intent.
Example 9:
class Message
{
var content: String
var sender: String
var receiver: String
init(content: String, sender: String, receiver: String)
{
self.content = content
self.sender = sender
self.receiver = receiver
}
convenience init(content: String)
{
self.init(content: content, sender: "Anonymous", receiver: "Anonymous")
}
func sendMessage()
{
print("\(content) sent by \(sender)")
print("\(content) received by \(receiver)")
}
}
Message(content: "Hello !", sender: "Kris", receiver: "Shiv").sendMessage()
Message(content: "Stay safe !").sendMessage()
Output 9:
Hello ! sent by Kris
Hello ! received by Shiv
Stay safe ! sent by Anonymous
Stay safe ! received by Anonymous
Two-phase Initialization
Class initialization in Swift is a two-phase process. In the first phase, each stored property is assigned an initial value by the class that introduced it. Once the initial state for every stored property has been determined, the second phase begins, and each class is given the opportunity to customize its stored properties further before the new instance is considered ready for use.
The use of a two-phase initialization process makes initialization safe, while still giving complete flexibility to each class in a class hierarchy. Two-phase initialization prevents property values from being accessed before they’re initialized, and prevents property values from being set to a different value by another initializer unexpectedly.
Swift’s compiler performs four helpful safety-checks to make sure that two-phase initialization is completed without error (Taken from Official Documentation):
Safety check 1
A designated initializer must ensure that all of the properties introduced by its class are initialized before it delegates up to a superclass initializer. As mentioned above, the memory for an object is only considered fully initialized once the initial state of all of its stored properties is known. In order for this rule to be satisfied, a designated initializer must make sure that all of its own properties are initialized before it hands off up the chain.
Safety check 2
A designated initializer must delegate up to a superclass initializer before assigning a value to an inherited property. If it doesn’t, the new value the designated initializer assigns will be overwritten by the superclass as part of its own initialization.
Safety check 3
A convenience initializer must delegate to another initializer before assigning a value to any property (including properties defined by the same class). If it doesn’t, the new value the convenience initializer assigns will be overwritten by its own class’s designated initializer.
Safety check 4
An initializer can’t call any instance methods, read the values of any instance properties, or refer to self as a value until after the first phase of initialization is complete. The class instance isn’t fully valid until the first phase ends. Properties can only be accessed, and methods can only be called, once the class instance is known to be valid at the end of the first phase.
Here’s how two-phase initialization plays out, based on the four safety checks above:
Phase 1
- A designated or convenience initializer is called on a class.
- Memory for a new instance of that class is allocated. The memory isn’t yet initialized.
- A designated initializer for that class confirms that all stored properties introduced by that class have a value. The memory for these stored properties is now initialized.
- The designated initializer hands off to a superclass initializer to perform the same task for its own stored properties.
- This continues up the class inheritance chain until the top of the chain is reached.
- Once the top of the chain is reached, and the final class in the chain has ensured that all of its stored properties have a value, the instance’s memory is considered to be fully initialized, and phase 1 is complete.
Phase 2
- Working back down from the top of the chain, each designated initializer in the chain has the option to customize the instance further. Initializers are now able to access self and can modify its properties, call its instance methods, and so on.
- Finally, any convenience initializers in the chain have the option to customize the instance and to work with self.
Automatic Initializer Inheritance
Subclasses don’t inherit their superclass initializers by default. However, superclass initializers are automatically inherited if certain conditions are met. In practice, this means that you don’t need to write initializer overrides in many common scenarios, and can inherit your superclass initializers with minimal effort whenever it’s safe to do so.
Assuming that you provide default values for any new properties you introduce in a subclass, the following two rules apply:
Rule 1
If your subclass doesn’t define any designated initializers, it automatically inherits all of its superclass designated initializers.
Rule 2
If your subclass provides an implementation of all of its superclass designated initializers—either by inheriting them as per rule 1, or by providing a custom implementation as part of its definition—then it automatically inherits all of the superclass convenience initializers.
These rules apply even if your subclass adds further convenience initializers.
Required Initializers
Swift allows to write the required
modifier before the definition of a class initializer to indicate that every subclass of the class must implement that initializer. You must also write the required
modifier before every subclass implementation of a required
initializer, to indicate that the initializer requirement applies to further subclasses in the chain. You don’t write the override
modifier when overriding a required designated initializer.
NOTE : You don’t have to provide an explicit implementation of a required
initializer if you can satisfy the requirement with an inherited initializer.
Example 10:
class Person
{
var name: String
var age: Int
required init(name: String, age: Int)
{
self.name = name
self.age = age
}
}
class Student: Person
{
var rollNumber: Int
required init(name: String, age: Int)
{
self.rollNumber = Int.random(in: 1...100)
super.init(name: name, age: age)
}
}
var student = Student(name: "Shri", age: 21)
print(student.rollNumber)
Output 10:
71
Access Levels’ impact on Inheritance
The following Table shows the impact of Access Levels on Inherited and Overridden Members:
Access Level of Members in Super Class | Can Members be Inherited | Allowed Access Level(s) of Overridden Non-Stored Properties and Methods in Subclass |
---|---|---|
private |
NO | Not Applicable |
fileprivate |
YES within same source file. NO in other source files. | fileprivate , internal , public , open within same module. Cannot be overridden in other modules. |
internal |
YES within any source file in the same module. NO in other modules. | internal , public , open within same module. Cannot be overridden in other modules. |
public |
YES within same module or any module that imports the defining module. | internal , public , open within same module. Cannot be overridden in other modules. |
open |
YES within same module or any module that imports the defining module. | internal , public , open |
The following table shows the impact of Access Levels on Inheritance of classes:
Access Level of Super Class | Allowed Access Level(s) of Subclass |
---|---|
private |
private , fileprivate within same module. Cannot be Inherited in other modules. |
fileprivate |
private , fileprivate within same module. Cannot be Inherited in other modules. |
internal |
private , fileprivate , internal within same module. Cannot be Inherited in other modules. |
public |
private , fileprivate , internal , public within same module. Cannot be Inherited in other modules. |
open |
private , fileprivate , internal , public , open in any module. |
Impact of Access Levels on Object creation using var
or let
The specifying class’ access level also affects the object creation of the class using var
or let
. The following table shows the access levels which objects must have:
Access Level of Class | Allowed Access Level(s) of Object |
---|---|
private |
private , fileprivate |
fileprivate |
private , fileprivate |
internal |
private , fileprivate , internal |
public |
private , fileprivate , internal , public |
open |
private , fileprivate , internal , public |
Preventing Overriding and Inheritance
- Overriding of Members within same module can be prevented by using
private
access level or by using thefinal
keyword on Property and Method declaration. - Overriding can be prevented in other modules by using any Non-
open
Access Level or by usingfinal
keyword on Property and Method declaration. - Inheritance within same module can be prevented by using
final
keyword on class declaration. - Inheritence can be prevented in other modules by using
final
keyword on class declaration or by using any Non-open
Access Level on Class declaration.
Subscripts
Classes, structures, and enumerations can define subscripts, which are shortcuts for accessing the member elements of a collection, list, or sequence. You use subscripts to set and retrieve values by index without needing separate methods for setting and retrieval. For example, you access elements in an Array
instance as someArray[index]
and elements in a Dictionary
instance as someDictionary[key]
.
You can define multiple subscripts for a single type, and the appropriate subscript overload to use is selected based on the type of index value you pass to the subscript. Subscripts aren’t limited to a single dimension, and you can define subscripts with multiple input parameters to suit your custom type’s needs.
Syntax :
subscript(index: Int) -> Int {
get {
// Return an appropriate subscript value here.
}
set(newValue) {
// Perform a suitable setting action here.
}
}
Example 11:
class MultiplicationTables
{
var multiplier: Int
init(of multiplier: Int)
{
self.multiplier = multiplier
}
subscript(index: Int) -> Int //Read-only subscript
{
index * multiplier
}
}
let elevenTables = MultiplicationTables(of: 11)
for x in 10...20
{
print("\(x) * 11 = \(elevenTables[x])") // Using Subscript syntax [] to get value
}
Output 11:
10 * 11 = 110
11 * 11 = 121
12 * 11 = 132
13 * 11 = 143
14 * 11 = 154
15 * 11 = 165
16 * 11 = 176
17 * 11 = 187
18 * 11 = 198
19 * 11 = 209
20 * 11 = 220
Subscripts can take any number of input parameters, and these input parameters can be of any type. Subscripts can also return a value of any type.
Like functions, subscripts can take a varying number of parameters and provide default values for their parameters, as discussed in Variadic Parameters and Default Parameter Values. However, unlike functions, subscripts can’t use inout
parameters.
A class or structure can provide as many subscript implementations as it needs, and the appropriate subscript to be used will be inferred based on the types of the value or values that are contained within the subscript brackets at the point that the subscript is used. This definition of multiple subscripts is known as Subscript Overloading.
While it’s most common for a subscript to take a single parameter, you can also define a subscript with multiple parameters if it’s appropriate for your type.
Example 12:
class RandomMatrix
{
var rows: Int, columns: Int, values: [Int]
init(rows: Int, columns: Int)
{
self.rows = rows
self.columns = columns
values = Array(repeating: 0, count: rows * columns)
for i in 0..<(rows * columns)
{
values[i] = Int.random(in: 1...100)
}
}
func indexIsValid(row: Int, column: Int) -> Bool
{
row >= 0 && row < rows && column >= 0 && column < columns
}
subscript(row: Int, column: Int) -> Int
{
get
{
assert(indexIsValid(row: row, column: column), "Index out of range")
return values[(row * columns) + column]
}
set
{
assert(indexIsValid(row: row, column: column), "Index out of range")
values[(row * columns) + column] = newValue
}
}
}
let squareMatrix = RandomMatrix(rows: 4, columns: 4)
for i in 0..<4
{
for j in 0..<4
{
print(squareMatrix[i, j],terminator: " ") // Using multiple parameter subscript
}
print()
}
Output 12:
37 71 99 67
25 44 24 53
36 53 25 35
91 59 61 75
Type Subscripts
The subscripts which we have seen so far are Instance subscripts. We can also define Type Subscripts of Classes, Structures and Enumerations using the static
keyword or class
keyword (For classes only).
Example 13:
class MultiplicationTables
{
var multiplier: Int
private init(of multiplier: Int)
{
self.multiplier = multiplier
}
subscript(index: Int) -> Int //Read-only subscript
{
index * multiplier
}
static subscript(multiplier: Int) -> MultiplicationTables //Read-only static subscript
{
MultiplicationTables(of: multiplier)
}
}
print("Tables of 9 and 10: ")
for x in 9...10
{
print("\(x) Tables")
for y in 1...10
{
print("\(x) * \(y) = \(MultiplicationTables[x][y])")
}
print()
}
Output 13:
Tables of 9 and 10:
9 Tables
9 * 1 = 9
9 * 2 = 18
9 * 3 = 27
9 * 4 = 36
9 * 5 = 45
9 * 6 = 54
9 * 7 = 63
9 * 8 = 72
9 * 9 = 81
9 * 10 = 90
10 Tables
10 * 1 = 10
10 * 2 = 20
10 * 3 = 30
10 * 4 = 40
10 * 5 = 50
10 * 6 = 60
10 * 7 = 70
10 * 8 = 80
10 * 9 = 90
10 * 10 = 100
Now that we have seen about Classes in Swift, let’s move on to see about Enumerations in Swift.