2017-02-01 23 views
5

tôi có mã sau thiết kế tổng thể của:sai chức năng chung chuyên ngành được gọi trong Swift 3 từ một cuộc gọi gián tiếp

protocol DispatchType {} 
class DispatchType1: DispatchType {} 
class DispatchType2: DispatchType {} 

func doBar<D:DispatchType>(value:D) { 
    print("general function called") 
} 

func doBar(value:DispatchType1) { 
    print("DispatchType1 called") 
} 

func doBar(value:DispatchType2) { 
    print("DispatchType2 called") 
} 

nơi trong thực tế DispatchType thực sự là một lưu trữ backend. Các chức năng doBar là các phương pháp tối ưu hóa phụ thuộc vào loại lưu trữ chính xác. Tất cả mọi thứ hoạt động tốt nếu tôi làm:

let d1 = DispatchType1() 
let d2 = DispatchType2() 

doBar(value: d1) // "DispatchType1 called" 
doBar(value: d2) // "DispatchType2 called" 

Tuy nhiên, nếu tôi thực hiện một chức năng mà các cuộc gọi doBar:

func test<D:DispatchType>(value:D) { 
    doBar(value: value) 
} 

và tôi cố gắng một mô hình gọi điện thoại tương tự, tôi nhận được:

test(value: d1)  // "general function called" 
test(value: d2)  // "general function called" 

Điều này có vẻ như một cái gì đó mà Swift sẽ có thể xử lý vì nó có thể xác định tại thời gian biên dịch các ràng buộc loại. Cũng như một bài kiểm tra nhanh, tôi cũng đã thử viết doBar là:

func doBar<D:DispatchType>(value:D) where D:DispatchType1 { 
    print("DispatchType1 called") 
} 

func doBar<D:DispatchType>(value:D) where D:DispatchType2 { 
    print("DispatchType2 called") 
} 

nhưng nhận được kết quả tương tự.

Bất kỳ ý tưởng nào nếu đây là hành vi Swift đúng, và nếu có, cách tốt để giải quyết hành vi này là gì?

Chỉnh sửa 1: Ví dụ về lý do tôi cố tránh sử dụng giao thức. Giả sử tôi có mã (đơn giản hóa từ mã thực tế của tôi):

protocol Storage { 
    // ... 
} 

class Tensor<S:Storage> { 
    // ... 
} 

Đối với lớp Tensor Tôi có một bộ cơ sở hoạt động có thể được thực hiện trên Tensor s. Tuy nhiên, bản thân các hoạt động sẽ thay đổi hành vi của chúng dựa trên lưu trữ. Hiện nay tôi thực hiện điều này với:

func dot<S:Storage>(_ lhs:Tensor<S>, _ rhs:Tensor<S>) -> Tensor<S> { ... } 

Trong khi tôi có thể đưa những trong Tensor lớp và sử dụng phần mở rộng:

extension Tensor where S:CBlasStorage { 
    func dot(_ tensor:Tensor<S>) -> Tensor<S> { 
     // ... 
    } 
} 

này có một vài tác dụng phụ mà tôi không thích:

  1. Tôi nghĩ rằng dot(lhs, rhs) thích hợp hơn là lhs.dot(rhs). Các chức năng tiện lợi có thể được viết để giải quyết vấn đề này, nhưng điều đó sẽ tạo ra một sự bùng nổ mã rất lớn.

  2. Điều này sẽ khiến lớp Tensor trở thành nguyên khối. Tôi thực sự thích có chứa số lượng mã tối thiểu cần thiết và mở rộng chức năng của nó bằng các chức năng phụ trợ.

  3. Liên quan đến (2), điều này có nghĩa là bất kỳ ai muốn thêm chức năng mới sẽ phải chạm vào lớp cơ sở mà tôi coi là thiết kế kém.

Chỉnh sửa 2: Một thay thế là những thứ làm việc dự kiến ​​nếu bạn sử dụng hạn chế đối với tất cả mọi thứ:

func test<D:DispatchType>(value:D) where D:DispatchType1 { 
    doBar(value: value) 
} 

func test<D:DispatchType>(value:D) where D:DispatchType2 { 
    doBar(value: value) 
} 

sẽ làm cho đúng doBar được gọi. Điều này cũng không phải là lý tưởng, vì nó sẽ gây ra rất nhiều mã bổ sung để được viết, nhưng ít nhất là cho phép tôi giữ thiết kế hiện tại của mình.

Chỉnh sửa 3: Tôi đã xem qua tài liệu cho biết việc sử dụng từ khóa static với Generics. Điều này giúp ít nhất là với điểm (1):

class Tensor<S:Storage> { 
    // ... 
    static func cos(_ tensor:Tensor<S>) -> Tensor<S> { 
     // ... 
    } 
} 

cho phép bạn viết:

let result = Tensor.cos(value) 

và nó hỗ trợ điều hành quá tải:

let result = value1 + value2 

nó có tính cách rườm rà gia tăng của yêu cầu Tensor. Điều này có thể làm tốt hơn một chút với:

typealias T<S:Storage> = Tensor<S> 
+0

Điều này xảy ra do cách thức Swift thực hiện phương thức gửi đi. Vui lòng xem https://www.raizlabs.com/dev/2016/12/swift-method-dispatch/ - đặc biệt là chương Reference Type Matters¨. – courteouselk

+0

Liên quan: [Mở rộng Bộ sưu tập với thuộc tính/phương pháp đệ quy phụ thuộc vào loại phần tử] (http://stackoverflow.com/q/41640321/2976878) – Hamish

+0

Hơi liên quan: [Cách gọi phương thức quá tải cụ thể hơn] (http://stackoverflow.com/questions/41531569/how-to-call-the-more-specific-method-of-overloading) – dfri

Trả lời

7

Đây thực sự là hành vi đúng khi quá trình giải quyết quá tải diễn ra vào thời gian thực hiện. Do đó, từ bên trong test(value:), điều duy nhất mà trình biên dịch biết về value là một loại nào đó phù hợp với DispatchType - do đó chỉ quá tải nó có thể gửi đến là func doBar<D : DispatchType>(value: D).

Mọi thứ sẽ khác nếu các hàm chung luôn được trình biên dịch đặc biệt, vì sau đó thực hiện chuyên ngành test(value:) sẽ biết loại bê tông value và do đó có thể chọn quá tải thích hợp. Tuy nhiên, chuyên môn hóa các chức năng chung hiện chỉ là một tối ưu hóa (như không có nội tuyến, nó có thể thêm sưng lên đáng kể cho mã của bạn), do đó, điều này không thay đổi hành vi được quan sát.

Một giải pháp để cho phép đa hình là tận dụng bảng nhân chứng giao thức (xem this great WWDC talk trên chúng) bằng cách thêm doBar() làm yêu cầu giao thức và triển khai thực hiện chuyên biệt của nó trong các lớp tương ứng tuân theo giao thức, với việc triển khai chung là một phần của phần mở rộng giao thức.

Điều này sẽ cho phép công văn động của doBar(), do đó cho phép nó được gọi từ test(value:) và thực hiện chính xác được gọi.

protocol DispatchType { 
    func doBar() 
} 

extension DispatchType { 
    func doBar() { 
     print("general function called") 
    } 
} 

class DispatchType1: DispatchType { 
    func doBar() { 
     print("DispatchType1 called") 
    } 
} 

class DispatchType2: DispatchType { 
    func doBar() { 
     print("DispatchType2 called") 
    } 
} 

func test<D : DispatchType>(value: D) { 
    value.doBar() 
} 

let d1 = DispatchType1() 
let d2 = DispatchType2() 

test(value: d1) // "DispatchType1 called" 
test(value: d2) // "DispatchType2 called" 
+0

Nó có thể là hành vi dự định, nhưng tôi chắc chắn sẽ không có dự kiến ​​nó! – Andreas

+0

Tôi đồng ý rằng đó không phải là hành vi mong đợi, ít nhất là đến từ các mẫu C++.Có cách nào để buộc các trình biên dịch để tạo ra các phiên bản riêng biệt và sử dụng chúng để giải quyết (tôi đã cố gắng '@ _specialize', nhưng điều đó đã không làm việc)? Tôi biết nếu tôi làm cho các phiên bản không chung chung của 'test' nó hoạt động, vì vậy trong lý thuyết Swift có khả năng hoạt động chính xác. Thật không may, bằng cách sử dụng giao thức làm cho thiết kế rất xấu xí, vì vậy tôi thà tránh làm điều đó ... –

+0

@AbeSchneider Tôi không * tin * hiện tại có một cách để buộc chuyên môn hóa một hàm chung nhất định - nó đơn giản được thực hiện như một tối ưu hóa bởi trình biên dịch. Mặc dù nó có giá trị [nộp đơn xin cải tiến] (https://bugs.swift.org) để xem liệu nhóm Swift có xem xét nó hay không. Cho rằng mã ban đầu của bạn sử dụng giao thức, tôi không chắc bạn thấy xấu về phương pháp thêm 'doBar()' làm yêu cầu - mặc dù cũng có thể là giải pháp tốt hơn cho trường hợp sử dụng cụ thể của bạn (có thể là định kiểu) . Mặc dù rất khó để nói mà không nhìn thấy một ví dụ cụ thể. – Hamish

Các vấn đề liên quan