In HarmonyOS Next development, the combination of generic programming, interfaces and classes can implement type-safe abstract logic and improve code reusability and maintainability.Through generic constraints (such as where T <: Interface
), developers can force types to follow specific contracts. This article combines the "Cangjie Programming Language Development Guide" to analyze the core application scenarios and practical points of generics in interfaces and classes.
1. Basic syntax and rules of generic constraints
Add constraints to generic types through the where
clause to ensure that the type meets interface implementation or class inheritance relationship:
func genericFunc<T>(param: T) where T <: SomeInterface & SomeClass {
// Constraint T must implement SomeInterface at the same time and inherit SomeClass
}
1. Interface constraints: Force type to implement specific behavior
interface Comparable {
func compare(to: T) -> Int
}
// Generic functions: Only accept types that implement Comparable interfaces
func sort<T: Comparable>(array: [T]) -> [T] {
array.sorted { $0.compare(to: $1) < 0 }
}
// Implementation class: Comparable interface must be implemented
class Number : Comparable {
let value: Int
public func compare(to: Number) -> Int {
value - to.value
}
}
2. Class constraints: Restricting the inheritance relationship of generic types
open class Base {}
class Derived : Base {}
// Generic functions: only accept subclasses of Base
func process<T: Base>(item: T) {
// Safe access to Base members
}
process(item: Derived()) // Legal: Derived is a Base subclass
2. In-depth collaboration scenarios between generics and interfaces
1. Generic calls to static members of interface
Use generic constraints to access static members of the interface to implement type-level logical abstraction:
interface Factory {
static func create(): Self // Self represents the implementation class itself
}
class Car : Factory {
public static func create(): Car { Car() }
}
// Generic functions: create objects through static members
func createInstance<T: Factory>(type: T.Type) -> T {
T.create() // Call the interface static factory method
}
let car: Car = createInstance(type: Car.self)
2. Multi-interface constraints: type requirements for compound capabilities
interface Connectable { func connect() }
interface Configurable { func configure() }
// Generic class: requires the type to implement two interfaces at the same time
class Manager<T: Connectable & Configurable> {
private let device: T
public init(device: T) { self.device = device }
public func setup() {
device.connect()
device.configure()
}
}
// Example of usage: Pass in to implement dual interface types
class NetworkDevice : Connectable, Configurable {
public func connect() {}
public func configure() {}
}
let manager = Manager(device: NetworkDevice())
3. Generic interface: Defining reusable contracts
interface Container<T> {
func add(item: T)
func remove(item: T) -> Bool
}
// Generic classes implement generic interfaces
class ListContainer<T> : Container<T> {
private var items: [T] = []
public func add(item: T) { items.append(item) }
public func remove(item: T) -> Bool {
if let index = items.firstIndex(of: item) {
items.remove(at: index)
return true
}
return false
}
}
3. Application of generics in class inheritance
1. Generic Base Class: Shared General Logic
open class DataProcessor<T> {
public func process(data: T) -> String {
data.description // Assume T implements description interface
}
}
// Subclass: Specify the specific type
class StringProcessor : DataProcessor<String> {
public override func process(data: String) -> String {
data.uppercased()
}
}
2. Covariance and Inverter: Compatibility of Collection Types
- Covariation: Subtypes are compatible when generic types are returned as return values
interface Animal {}
class Dog : Animal {}
func getAnimals() -> [Animal] { [Dog()] } // Legal: [Dog] is the covariant type of [Animal]
- Inverter: When a generic type is used as a parameter, the parent type is compatible
func feed(animals: [Animal]) {}
let dogs: [Dog] = [Dog()]
feed(animals: dogs) // Legal: [Animal] compatible with [Dog] (inverter)
3. Generic type erasure and runtime check
Generic types will be erased at runtime and need to be type checked through the is
operator:
func printType<T>(item: T) {
if item is Int {
println("type is Int")
} else if item is String {
println("type is String")
}
}
4. Practical scenario: General data storage module design
Scenario: Build a general storage interface that supports multiple data formats (JSON/XML/binary), and use generic constraints to achieve type safety.
1. Define generic interfaces and constraints
interface DataFormat {
associatedtype Item // Associated type, define the data item type
static func encode(item: Item) -> Data
static func decode(data: Data) -> Item
}
// Generic storage class
class Storage<T: DataFormat> {
public func save(item: T.Item) -> Data {
T.encode(item: item) // Call static interface method
}
public func load(data: Data) -> T.Item {
T.decode(data: data)
}
}
2. Implementation of specific data formats
// JSON format implementation
struct JSONFormat : DataFormat {
typealias Item = Dictionary<String, Any>
public static func encode(item: Item) -> Data {
// JSON encoding logic
}
public static func decode(data: Data) -> Item {
// JSON decoding logic
}
}
// XML format implementation
struct XMLFormat : DataFormat {
typealias Item = String
public static func encode(item: Item) -> Data {
// XML encoding logic
}
public static func decode(data: Data) -> Item {
// XML decoding logic
}
}
3. Generic calls and type safety
let jsonStorage = Storage<JSONFormat>()
let xmlStorage = Storage<XMLFormat>()
// Save JSON data
let jsonData = jsonStorage.save(item: ["key": "value"])
// Load XML data
let xmlItem = xmlStorage.load(data: xmlData)
5. Design principles and trap avoidance
1. Avoid overconstraints: Maintain generic flexibility
// Counterexample: Too many interface constraints limit reusability
func process<T: InterfaceA & InterfaceB & InterfaceC>(item: T) { ... }
// Positive example: Split into multiple generic functions or use a combination of protocols
func processA<T: InterfaceA>(item: T) { ... }
func processB<T: InterfaceB>(item: T) { ... }
2. Limitations of Type Erase
Avoid running-time features that depend on specific types in generic functions:
func printGenericType<T>(item: T) {
// Counterexample: Cannot get the specific type name of T at runtime (type erase)
println(type(of: item)) // Only generic placeholders can be obtained
}
3. Association type and protocol extension
Use the association type to add generic constraints to the interface, and provide a default implementation through extension:
interface Collection {
associatedtype Element
func append(element: Element)
}
// Extend Collection interface for Array
extend Array : Collection {
public func append(element: Element) { self.append(element) }
}
6. Summary: The abstract power of generic constraints
The combination of generics, interfaces and classes in HarmonyOS Next implements the following core capabilities:
- Type Safety: The compiler forces to check whether the type meets the constraints to avoid runtime errors;
- Code reuse: A set of logic processes multiple types to reduce duplicate implementations;
- Abstract extension: Define generic behavior through interface contracts to improve system scalability.
Top comments (0)