Các lớp trừu tượng trong Ngôn ngữ Swift


140

Có cách nào để tạo một lớp trừu tượng trong Ngôn ngữ Swift hay đây là một giới hạn giống như Objective-C? Tôi muốn tạo một lớp trừu tượng có thể so sánh với những gì Java định nghĩa là một lớp trừu tượng.


Bạn có cần lớp đầy đủ để trừu tượng hay chỉ một số phương thức trong đó? Xem câu trả lời ở đây cho các phương pháp và thuộc tính duy nhất. stackoverflow.com/a/39038828/2435872 . Trong Java, bạn có thể tạo các lớp trừu tượng, không có phương thức nào trừu tượng. Tính năng đặc biệt đó không được Swift cung cấp.
jboi

Câu trả lời:


174

Không có các lớp trừu tượng trong Swift (giống như Objective-C). Đặt cược tốt nhất của bạn sẽ là sử dụng Giao thức , giống như Giao diện Java.

Với Swift 2.0, sau đó bạn có thể thêm các cài đặt phương thức và các cài đặt thuộc tính được tính bằng các phần mở rộng giao thức. Hạn chế duy nhất của bạn là bạn không thể cung cấp các biến hoặc hằng thành viênkhông có công văn động .

Một ví dụ về kỹ thuật này sẽ là:

protocol Employee {
    var annualSalary: Int {get}
}

extension Employee {
    var biweeklySalary: Int {
        return self.annualSalary / 26
    }

    func logSalary() {
        print("$\(self.annualSalary) per year or $\(self.biweeklySalary) biweekly")
    }
}

struct SoftwareEngineer: Employee {
    var annualSalary: Int

    func logSalary() {
        print("overridden")
    }
}

let sarah = SoftwareEngineer(annualSalary: 100000)
sarah.logSalary() // prints: overridden
(sarah as Employee).logSalary() // prints: $100000 per year or $3846 biweekly

Lưu ý rằng điều này đang cung cấp "lớp trừu tượng" như các tính năng ngay cả đối với các cấu trúc, nhưng các lớp cũng có thể thực hiện cùng một giao thức.

Cũng lưu ý rằng mỗi lớp hoặc cấu trúc thực hiện giao thức Nhân viên sẽ phải khai báo lại thuộc tính hàng năm.

Quan trọng nhất, lưu ý rằng không có công văn năng động . Khi logSalaryđược gọi trên cá thể được lưu trữ dưới dạng SoftwareEngineernó sẽ gọi phiên bản ghi đè của phương thức. Khi logSalaryđược gọi trong trường hợp sau khi nó được chuyển sang một Employee, nó gọi thực hiện ban đầu (nó không tự động gửi đến phiên bản bị ghi đè mặc dù thực tế đó là một Software Engineer.

Để biết thêm thông tin, hãy xem video WWDC tuyệt vời về tính năng đó: Xây dựng ứng dụng tốt hơn với các loại giá trị trong Swift


3
protocol Animal { var property : Int { get set } }. Bạn cũng có thể rời khỏi tập hợp nếu bạn không muốn tài sản có setter
drewag

3
Tôi nghĩ rằng video wwdc này thậm chí còn phù hợp hơn
Mario Zannone

2
@MarioZannone rằng video đó đã thổi vào tâm trí tôi và khiến tôi yêu Swift.
Scott H

3
Nếu bạn chỉ cần thêm func logSalary()vào khai báo giao thức Nhân viên, ví dụ sẽ in overriddencho cả hai cuộc gọi đến logSalary(). Đây là trong Swift 3.1. Do đó bạn có được những lợi ích của đa hình. Phương pháp đúng được gọi trong cả hai trường hợp.
Mike Cheesne

1
Quy tắc về công văn động là thế này ... nếu phương thức chỉ được định nghĩa trong phần mở rộng, thì nó được gửi tĩnh. Nếu nó cũng được xác định trong giao thức bạn đang mở rộng, thì nó sẽ tự động được gửi đi. Không cần thời gian chạy Objective-C. Đây là hành vi thuần túy của Swift.
Đánh dấu A. Donohoe

47

Lưu ý rằng câu trả lời này được nhắm mục tiêu tại Swift 2.0 trở lên

Bạn có thể đạt được hành vi tương tự với các giao thức và phần mở rộng giao thức.

Đầu tiên, bạn viết một giao thức hoạt động như một giao diện cho tất cả các phương thức phải được thực hiện trong tất cả các loại phù hợp với nó.

protocol Drivable {
    var speed: Float { get set }
}

Sau đó, bạn có thể thêm hành vi mặc định cho tất cả các loại phù hợp với nó

extension Drivable {
    func accelerate(by: Float) {
        speed += by
    }
}

Bây giờ bạn có thể tạo các loại mới bằng cách thực hiện Drivable.

struct Car: Drivable {
    var speed: Float = 0.0
    init() {}
}

let c = Car()
c.accelerate(10)

Vì vậy, về cơ bản bạn nhận được:

  1. Biên dịch kiểm tra thời gian đảm bảo rằng tất cả Drivables thực hiệnspeed
  2. Bạn có thể triển khai hành vi mặc định cho tất cả các loại phù hợp với Drivable( accelerate)
  3. Drivable được đảm bảo không bị khởi tạo vì nó chỉ là một giao thức

Mô hình này thực sự hoạt động giống nhiều đặc điểm hơn, có nghĩa là bạn có thể tuân thủ nhiều giao thức và thực hiện các triển khai mặc định của bất kỳ giao thức nào, trong khi với siêu lớp trừu tượng, bạn bị giới hạn trong hệ thống phân cấp lớp đơn giản.


Tuy nhiên, không phải lúc nào cũng có khả năng mở rộng một số giao thức, ví dụ , UICollectionViewDatasource. Tôi muốn loại bỏ tất cả các mẫu soạn sẵn và gói nó trong giao thức / phần mở rộng riêng biệt và sau đó sử dụng lại bởi nhiều lớp. Trên thực tế, mẫu mẫu sẽ hoàn hảo ở đây, nhưng ...
Richard Topchii

1
Bạn không thể ghi đè accelate˚ trong ˚Car˚. Nếu bạn làm như vậy, việc thực hiện trong ˚extentsion Drivizable˚ vẫn được gọi mà không có bất kỳ cảnh báo nào của trình biên dịch. Rất không giống như một lớp trừu tượng Java
Gerd Castan

@GerdCastan Đúng, tiện ích mở rộng giao thức không hỗ trợ công văn động.
IluTov

15

Tôi nghĩ rằng đây là gần nhất với Java abstracthoặc C # abstract:

class AbstractClass {

    private init() {

    }
}

Lưu ý rằng, để các công cụ privatesửa đổi hoạt động, bạn phải định nghĩa lớp này trong một tệp Swift riêng.

EDIT: Tuy nhiên, mã này không cho phép khai báo một phương thức trừu tượng và do đó buộc thực thi nó.


4
Tuy nhiên, điều này không buộc một lớp con ghi đè một hàm trong khi cũng có một triển khai cơ sở của hàm đó trong lớp cha.
Matthew Quiros

Trong C #, nếu bạn triển khai một hàm trong một lớp cơ sở trừu tượng, bạn không bị buộc phải thực hiện nó trong các lớp con của nó. Tuy nhiên, mã này không cho phép bạn khai báo một phương thức trừu tượng để buộc ghi đè.
Teejay

Hãy nói rằng lớp con ConcreteClass mà AbstractClass. Làm thế nào để bạn khởi tạo ConcreteClass?
Javier Cadiz

2
ConcreteClass nên có một nhà xây dựng công cộng. Bạn có thể cần một hàm tạo được bảo vệ trong AbstractClass, trừ khi chúng nằm trong cùng một tệp. Theo những gì tôi nhớ, công cụ sửa đổi truy cập được bảo vệ không tồn tại trong Swift. Vì vậy, giải pháp là khai báo ConcreteClass trong cùng một tệp.
Teejay

13

Cách đơn giản nhất là sử dụng lệnh gọi fatalError("Not Implemented")vào phương thức trừu tượng (không phải biến) trên phần mở rộng giao thức.

protocol MyInterface {
    func myMethod() -> String
}


extension MyInterface {

    func myMethod() -> String {
        fatalError("Not Implemented")
    }

}

class MyConcreteClass: MyInterface {

    func myMethod() -> String {
        return "The output"
    }

}

MyConcreteClass().myMethod()

Đây là một câu trả lời tuyệt vời. Tôi không nghĩ rằng nó sẽ hoạt động nếu bạn gọi (MyConcreteClass() as MyInterface).myMethod()nhưng nó làm! Khóa này bao gồm myMethodtrong khai báo giao thức; Nếu không thì cuộc gọi gặp sự cố.
Mike Cheesne

11

Sau khi tôi vật lộn trong vài tuần, cuối cùng tôi cũng nhận ra cách dịch một lớp trừu tượng Java / PHP sang Swift:

public class AbstractClass: NSObject {

    internal override init(){}

    public func getFoodToEat()->String
    {
        if(self._iAmHungry())
        {
            return self._myFavoriteFood();
        }else{
            return "";
        }
    }

    private func _myFavoriteFood()->String
    {
        return "Sandwich";
    }

    internal func _iAmHungry()->Bool
    {
        fatalError(__FUNCTION__ + "Must be overridden");
        return false;
    }
}

public class ConcreteClass: AbstractClass, IConcreteClass {

    private var _hungry: Bool = false;

    public override init() {
        super.init();
    }

    public func starve()->Void
    {
        self._hungry = true;
    }

    public override func _iAmHungry()->Bool
    {
        return self._hungry;
    }
}

public protocol IConcreteClass
{
    func _iAmHungry()->Bool;
}

class ConcreteClassTest: XCTestCase {

    func testExample() {

        var concreteClass: ConcreteClass = ConcreteClass();

        XCTAssertEqual("", concreteClass.getFoodToEat());

        concreteClass.starve();

        XCTAssertEqual("Sandwich", concreteClass.getFoodToEat());
    }
}

Tuy nhiên, tôi nghĩ rằng Apple đã không triển khai các lớp trừu tượng bởi vì nó thường sử dụng mẫu giao thức ủy nhiệm + thay thế. Ví dụ, mẫu tương tự ở trên sẽ được thực hiện tốt hơn như thế này:

import UIKit

    public class GoldenSpoonChild
    {
        private var delegate: IStomach!;

        internal init(){}

        internal func setup(delegate: IStomach)
        {
            self.delegate = delegate;
        }

        public func getFoodToEat()->String
        {
            if(self.delegate.iAmHungry())
            {
                return self._myFavoriteFood();
            }else{
                return "";
            }
        }

        private func _myFavoriteFood()->String
        {
            return "Sandwich";
        }
    }

    public class Mother: GoldenSpoonChild, IStomach
    {

        private var _hungry: Bool = false;

        public override init()
        {
            super.init();
            super.setup(self);
        }

        public func makeFamilyHungry()->Void
        {
            self._hungry = true;
        }

        public func iAmHungry()->Bool
        {
            return self._hungry;
        }
    }

    protocol IStomach
    {
        func iAmHungry()->Bool;
    }

    class DelegateTest: XCTestCase {

        func testGetFood() {

            var concreteClass: Mother = Mother();

            XCTAssertEqual("", concreteClass.getFoodToEat());

            concreteClass.makeFamilyHungry();

            XCTAssertEqual("Sandwich", concreteClass.getFoodToEat());
        }
    }

Tôi cần loại mẫu này vì tôi muốn phổ biến một số phương thức trong UITableViewControll như viewWillAppear, v.v ... Điều này có hữu ích không?


1
+1 Đã lên kế hoạch thực hiện chính xác cách tiếp cận mà bạn đề cập trước tiên; con trỏ thú vị cho mô hình đoàn.
Angad

Ngoài ra, nó sẽ giúp nếu cả hai ví dụ của bạn trong cùng một trường hợp sử dụng. GoldenSpoonChild là một cái tên hơi khó hiểu, đặc biệt là mẹ dường như đang mở rộng nó.
Angad

@Angad Mẫu đại biểu là trường hợp sử dụng tương tự, tuy nhiên nó không phải là bản dịch; đó là một mô hình khác nhau nên nó phải có một góc nhìn khác.
Josh Woodcock

8

Có một cách để mô phỏng các lớp trừu tượng bằng cách sử dụng Giao thức. Đây là một ví dụ:

protocol MyProtocol {
   func doIt()
}

class BaseClass {
    weak var myDelegate: MyProtocol?

    init() {
        ...
    }

    func myFunc() {
        ...
        self.myDelegate?.doIt()
        ...
    }
}

class ChildClass: BaseClass, MyProtocol {
    override init(){
        super.init()
        self.myDelegate = self
    }

    func doIt() {
        // Custom implementation
    }
}

1

Một cách nữa để bạn có thể thực hiện lớp trừu tượng là chặn trình khởi tạo. Tôi đã làm theo cách này:

class Element:CALayer { // IT'S ABSTRACT CLASS

    override init(){ 
        super.init()
        if self.dynamicType === Element.self {
        fatalError("Element is abstract class, do not try to create instance of this class")
        }
    }
}

4
Điều này không cung cấp bất kỳ đảm bảo và / hoặc kiểm tra. Thổi lên trong thời gian chạy là một cách xấu để thực thi các quy tắc. Tốt hơn là có init là riêng tư.
Морт

Các lớp trừu tượng cũng cần có sự hỗ trợ cho các phương thức trừu tượng.
Cristik

@Cristik Tôi đã chỉ ra ý chính, đó không phải là giải pháp hoàn chỉnh. Bằng cách này, bạn có thể không thích 80% câu trả lời vì chúng không đủ chi tiết cho tình huống của bạn
Alexey Yarmolovich

1
@AlexeyYarmolovich ai nói tôi không thích 80% câu trả lời? :) Đùa qua một bên, tôi đã gợi ý rằng ví dụ của bạn có thể được cải thiện, điều này sẽ giúp những người đọc khác, và sẽ giúp bạn bằng cách nhận được sự ủng hộ.
Cristik

0

Tôi đã cố gắng tạo một Weatherlớp trừu tượng, nhưng sử dụng các giao thức không lý tưởng vì tôi phải viết cùng một initphương thức nhiều lần. Việc mở rộng giao thức và viết một initphương thức có vấn đề, đặc biệt là khi tôi đang sử dụng NSObjecttuân thủ NSCoding.

Vì vậy, tôi đã đưa ra điều này cho sự NSCodingphù hợp:

required init?(coder aDecoder: NSCoder) {
    guard type(of: self) != Weather.self else {
        fatalError("<Weather> This is an abstract class. Use a subclass of `Weather`.")
    }
    // Initialize...
}        

Đối với init:

fileprivate init(param: Any...) {
    // Initialize
}

0

Di chuyển tất cả các tham chiếu đến các thuộc tính và phương thức trừu tượng của lớp Cơ sở sang triển khai mở rộng giao thức, trong đó Tự ràng buộc với lớp Cơ sở. Bạn sẽ có quyền truy cập vào tất cả các phương thức và thuộc tính của lớp Base. Ngoài ra trình biên dịch kiểm tra việc thực hiện các phương thức và thuộc tính trừu tượng trong giao thức cho các lớp dẫn xuất

protocol Commom:class{
  var tableView:UITableView {get};
  func update();
}

class Base{
   var total:Int = 0;
}

extension Common where Self:Base{
   func update(){
     total += 1;
     tableView.reloadData();
   }
} 

class Derived:Base,Common{
  var tableView:UITableView{
    return owner.tableView;
  }
}

0

Với giới hạn không có công văn động, bạn có thể làm một cái gì đó như thế này:

import Foundation

protocol foo {

    static var instance: foo? { get }
    func prt()

}

extension foo {

    func prt() {
        if Thread.callStackSymbols.count > 30 {
            print("super")
        } else {
            Self.instance?.prt()
        }
    }

}

class foo1 : foo {

    static var instance : foo? = nil

    init() {
        foo1.instance = self
    }

    func prt() {
        print("foo1")
    }

}

class foo2 : foo {

    static var instance : foo? = nil

    init() {
        foo2.instance = self
    }

    func prt() {
        print("foo2")
    }

}

class foo3 : foo {

    static var instance : foo? = nil

    init() {
        foo3.instance = self
    }

}

var f1 : foo = foo1()
f1.prt()
var f2 : foo = foo2()
f2.prt()
var f3 : foo = foo3()
f3.prt()
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.