简体   繁体   中英

Generics in Swift

I am learning about Generics in Swift. For me, this topic is quite hard to understand. In the book I am reading, there is 2 challenges on Generics:

1st challenge : it asks to write a function findAll(_:_:) that takes and array of any type T that conforms to the Equatable protocol and a single element (also of type T). findAll(_:_:) should return an array of integers corresponding to every location where the element was found in the array. For example, findAll([5,3,7,3,9], 3] should return [1,3] .

2nd challenge : to modify findAll(_:_:) to accept a Collection instead of an array and it gives a hint "You will need to change the return type from [Int] to an array of an associated type of the Collection protocol"

This is what i have done for first challenge

func findAll<T:Equatable> (_ first: [T], _ second: T) -> [Int] {
var array = [Int]()

for i in 0..<first.count {
    if first[i] == second {
        array.append(i)
        }
    }   
return array
}

For the second challenge, what i am thinking about is a generic function that I can pass a Collection (can be an Array, a Dictionary or a Set). But for Set type, as it does not have a defined ordering, how do you find location of an item in a Set ?

Thank you.

The subscript method of collections is defined as

public subscript(position: Self.Index) -> Self.Iterator.Element { get }

which means that your function should take as arguments

  • a collection C , and
  • a value of the associated type C.Iterator.Element

and return an array of C.Index . In addition, the element type should be Equatable :

func findAll<C: Collection> (_ collection: C, _ element: C.Iterator.Element) -> [C.Index]
    where C.Iterator.Element: Equatable
{ ... }

Similar as in your solution for arrays, one can loop over the collection's indices:

func findAll<C: Collection> (_ collection: C, _ element: C.Iterator.Element) -> [C.Index]
where C.Iterator.Element: Equatable
{
    var result: [C.Index] = []

    var idx = collection.startIndex
    while idx != collection.endIndex {
        if collection[idx] == element {
            result.append(idx)
        }
        collection.formIndex(after: &idx)
    }

    return result
}

One would expect that something like

for idx in collection.startIndex ..< collection.endIndex
// or
for idx in collection.indices

works, but (in Swift 3) this requires an additional constraint on the associated Indices type:

func findAll<C: Collection> (_ collection: C, _ element: C.Iterator.Element) -> [C.Index]
    where C.Iterator.Element: Equatable, C.Indices.Iterator.Element == C.Index
{

    var result: [C.Index] = []

    for idx in collection.indices {
        if collection[idx] == element {
            result.append(idx)
        }
    }

    return result
}

This is no longer necessary in Swift 4, see for example Unable to use indices.contains() in a Collection extension in Swift 3 for a good explanation.

This can now be simplified using filter :

func findAll<C: Collection> (_ collection: C, _ element: C.Iterator.Element) -> [C.Index]
    where C.Iterator.Element: Equatable, C.Indices.Iterator.Element == C.Index
{
    return collection.indices.filter { collection[$0] == element }
}

Example (a collection of Character ):

let chars = "abcdabcdabcd".characters
let indices = findAll(chars, "c")
for idx in indices {
    print(chars[idx])
}

Set is a Collection as well, it has an associated Index type and a subscript method. Example:

let set = Set([1, 2, 3, 4, 5, 6, 7, 8, 9])
let indices = findAll(set, 3)
for idx in indices {
    print(set[idx])
}

Finally you might want to define the function as a method on the Collection type:

extension Collection where Iterator.Element: Equatable, Indices.Iterator.Element == Index {
    func allIndices(of element: Iterator.Element) -> [Index] {
        return indices.filter { self[$0] == element }
    }
}

// Example:
let indices = [1, 2, 3, 1, 2, 3].allIndices(of: 3)

In order to create a custom definition of a function, you should create an extension on the type which the function belongs to. From there, the override tag must be used so that you can create a custom implementation for the function. More specifically, to create a function that accepts a collection instead of an array, create an overridden version of the function that accepts a collection instead.

Also please show us what you've tried so far, instead of just saying I've tried several things.

Here are some links that may be useful:

Swift Documentation

Override function

Another simple example of an override is whenever a ViewContoller class is made, the viewDidLoad() method where view setup happens in is often overridden.

Generics was quite a headache for me when I started learning it in the beginning. Though after some dedicated research in this topic I came across [this] 1 nice tutorial which helped me understanding this topic little deeper. Here I'm sharing the demo code which I'd prepared while learning, hope that help someone.

Demo contains UITableview with different type of cells, each UITableview represents single UITableViewCell with associated Model. I've also added one Hybrid Tableview in order to mix different types of cell in single tableview.

Here is the code.

Creating Generic UITableViewCells first

protocol ProtocolCell {
    associatedtype U
    static var cellIdentifier:String { get }
    func configure(item:U,indexPath:IndexPath)
}

class BaseCell<U>: UITableViewCell, ProtocolCell {
    
    var item:U!
    
    static var cellIdentifier: String {
        return String(describing: self)
    }
    
    func configure(item:U,indexPath: IndexPath) {
        textLabel?.text = "Row: \(indexPath.row), \tClass: \(U.self)  OVERRIDE THIS !!"
        backgroundColor = .red
    }

    //MARK:- INIT
    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    func commonInit(){
        selectionStyle = .none
    }
}

class StringCell: BaseCell<String> {
    
    override func configure(item: String, indexPath: IndexPath) {
        super.configure(item: item, indexPath: indexPath)
        textLabel?.text = "Row: \(indexPath.row), \tClass: \(U.self), \tData: \(item)"
        backgroundColor = UIColor.yellow.withAlphaComponent(0.3)
    }
}

class DogCell: BaseCell<Dog> {
    
    override func configure(item: Dog, indexPath: IndexPath) {
        super.configure(item: item, indexPath: indexPath)
        textLabel?.text = "Row: \(indexPath.row), \tClass: \(U.self), \tData: \(item.name)"
        backgroundColor = UIColor.green.withAlphaComponent(0.3)
    }
}

class CountryCell: BaseCell<Country> {
    
    override func configure(item: Country, indexPath: IndexPath) {
        super.configure(item: item, indexPath: indexPath)
        textLabel?.text = "Row: \(indexPath.row), \tClass: \(U.self), \tData: \(item.name)"
        backgroundColor = UIColor.magenta.withAlphaComponent(0.3)
    }
}

Then creating Generic UITableViews

class BaseTableView<T_Cell:BaseCell<U_Model>,U_Model>: UITableView, UITableViewDelegate, UITableViewDataSource {

    var arrDataSource = [U_Model]()
    var blockDidSelectRowAt:((IndexPath) -> Void)?
    
    override init(frame: CGRect, style: UITableViewStyle) {
        super.init(frame: frame, style: style)
        commonInit()
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    func addIn(view:UIView) {

        view.addSubview(self)
        translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            leftAnchor.constraint(equalTo: view.readableContentGuide.leftAnchor),
            rightAnchor.constraint(equalTo: view.readableContentGuide.rightAnchor),
            bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
            ])
    }
    func commonInit() {
        delegate = self
        dataSource = self
        backgroundColor = .gray
        layer.borderWidth = 2
        layer.borderColor = UIColor.red.cgColor
        register(T_Cell.self, forCellReuseIdentifier: T_Cell.cellIdentifier)
    }
    
    //MARK:- DATA SOURCE
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return arrDataSource.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: T_Cell.cellIdentifier, for: indexPath) as! BaseCell<U_Model>
        cell.configure(item: arrDataSource[indexPath.row], indexPath: indexPath)
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        blockDidSelectRowAt?(indexPath)
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 50
    }
}

class DogTableView: BaseTableView<DogCell, Dog> {
    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 70
    }
}

class StringTableView: BaseTableView<StringCell, String> {
    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 90
    }
}

class CountryTableView: BaseTableView<CountryCell, Country> {
    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 100
    }
}

class HybridTableView: BaseTableView<BaseCell<Any>, Any> {

    // OVERRIDING DEFAULT BEHAVIOUR
    override func commonInit() {
        super.commonInit()
        
        register(DogCell.self, forCellReuseIdentifier: DogCell.cellIdentifier)
        register(StringCell.self, forCellReuseIdentifier: StringCell.cellIdentifier)
        register(CountryCell.self, forCellReuseIdentifier: CountryCell.cellIdentifier)
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        let hybridItem = arrDataSource[indexPath.row]
        
        switch hybridItem {
        
        case is Dog:
            let cell = tableView.dequeueReusableCell(withIdentifier: DogCell.cellIdentifier) as! DogCell
            cell.configure(item: hybridItem as! Dog, indexPath: indexPath)
            return cell
        case is String:
            let cell = tableView.dequeueReusableCell(withIdentifier: StringCell.cellIdentifier) as! StringCell
            cell.configure(item: hybridItem as! String, indexPath: indexPath)
            return cell
        case is Country:
            let cell = tableView.dequeueReusableCell(withIdentifier: CountryCell.cellIdentifier) as! CountryCell
            cell.configure(item: hybridItem as! Country, indexPath: indexPath)
            return cell
        default:
            let cell = tableView.dequeueReusableCell(withIdentifier: BaseCell<Any>.cellIdentifier) as! BaseCell<Any>
            cell.configure(item: hybridItem, indexPath: indexPath)
            return cell
        }
    }
    
    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        
        let hybridItem = arrDataSource[indexPath.row]
        
        switch hybridItem {
            
        case is Dog:        return 70
        case is String:     return 140
        case is Country:    return 210
        default:            return 50
        }
    }
}

Models used in the tableview

struct Dog {
    let name : String
}

struct Country {
    let name : String
}

ViewController

class ViewController: UIViewController {

    //MARK:- OUTLETS
    @IBOutlet private weak var btn: UIButton!

    private let tableViewDog = DogTableView()
    private let tableViewString = StringTableView()
    private let tableViewCountry = CountryTableView()
    
    private let tableViewHybrid = HybridTableView()
    
    
    //MARK:- VIEW LIFE CYCLE
    override func viewDidLoad() {
        super.viewDidLoad()
        
//        doSetupUI()
        doSetupUIHybrid()
    }
    
    //MARK:- UI SETUP
    private func doSetupUI(){
        tableViewDog.addIn(view: view)

        tableViewDog.arrDataSource = [Dog(name: "Dog 1"),Dog(name: "Dog 2")]
//        tableView.arrDataSource = ["First","Second"]
//        tableView.arrDataSource = [Country(name: "India"),Country(name: "Nepal")]
        
        tableViewDog.reloadData()
        tableViewDog.blockDidSelectRowAt = { [unowned selff = self] indexPath in
            
            print("DID SELECT ROW : \(indexPath.row), VALUE : \(selff.tableViewDog.arrDataSource[indexPath.row].name)")
        }
    }
    
    private func doSetupUIHybrid(){
        tableViewHybrid.addIn(view: view)
        
        tableViewHybrid.arrDataSource = [Dog(name: "Dog1"),
                                   "String1",
                                   Country(name: "India"),
                                   Dog(name: "Dog2"),
                                   "String2",
                                   Country(name: "Nepal")]
        tableViewHybrid.reloadData()
        tableViewHybrid.blockDidSelectRowAt = { [unowned selff = self] indexPath in
            
            var itemToPrint = ""
            let hybridItem = selff.tableViewHybrid.arrDataSource[indexPath.row]
            switch hybridItem {
            case is Dog:    itemToPrint = (hybridItem as! Dog).name
            case is Country:    itemToPrint = (hybridItem as! Country).name
            case is String:    itemToPrint = (hybridItem as! String)
            default:    break
            }
            print("DID SELECT ROW : \(indexPath.row), VALUE : \(itemToPrint)")
        }
    }
    
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM