Variance
Understand how variance (covariance and contravariance) works with generics in Dart.
What is it?
Variance describes how subtyping between generic types relates to subtyping between their type arguments. In Dart, generics are invariant by default, but covariance and contravariance can be specified using the covariant keyword.
Why does it exist?
Variance exists to:
- Understand type relationships in generics
- Enable type-safe method overriding
- Handle collections polymorphically
- Support flexible APIs
- Maintain type safety
- Prevent runtime errors
Covariance
What is Covariance?
// Covariance: If A is subtype of B, then Generic<A> is subtype of Generic<B>
// This means you can use Generic<Dog> where Generic<Animal> is expected
class Animal {
void makeSound() => print('Animal sound');
}
class Dog extends Animal {
@override
void makeSound() => print('Woof!');
void fetch() => print('Fetching...');
}
// Covariance demonstration
void processAnimals(List<Animal> animals) {
for (var animal in animals) {
animal.makeSound();
}
}
void main() {
// This works with covariance
List<Dog> dogs = [Dog(), Dog()];
processAnimals(dogs); // Covariance allows this
}
Covariance with Collections
class AnimalShelter {
List<Animal> animals = [];
void addAnimal(Animal animal) {
animals.add(animal);
}
List<Animal> getAnimals() => animals;
}
class DogShelter extends AnimalShelter {
@override
void addAnimal(covariant Dog animal) {
super.addAnimal(animal);
print('Added dog: ${animal.runtimeType}');
}
@override
List<Dog> getAnimals() {
// Covariant return type
return animals.whereType<Dog>().toList();
}
}
// Usage
var shelter = DogShelter();
shelter.addAnimal(Dog()); // Added dog: Dog
// shelter.addAnimal(Cat()); // Error! Cat is not covariant
var animals = shelter.getAnimals(); // Returns List<Dog>
Covariant Parameters
class Box<T> {
final T value;
Box(this.value);
void process(T value) {
print('Processing: $value');
}
}
class StringBox extends Box<String> {
StringBox(String value) : super(value);
@override
void process(covariant Object value) {
// Covariant allows broader type
if (value is String) {
print('Processing string: $value');
} else {
print('Processing object: $value');
}
}
}
// Usage
var box = StringBox('Hello');
box.process('World'); // Processing string: World
box.process(42); // Processing object: 42
Contravariance
What is Contravariance?
// Contravariance: If A is subtype of B, then Generic<B> is subtype of Generic<A>
// This means you can use Generic<Animal> where Generic<Dog> is expected
abstract class Comparator<T> {
int compare(T a, T b);
}
// Covariant comparators
class AnimalComparator implements Comparator<Animal> {
@override
int compare(Animal a, Animal b) {
return a.runtimeType.toString().compareTo(b.runtimeType.toString());
}
}
class DogComparator extends AnimalComparator {
@override
int compare(covariant Dog a, covariant Dog b) {
// More specific comparison
return a.toString().compareTo(b.toString());
}
}
// Usage
void compareDogs(Comparator<Dog> comparator, Dog a, Dog b) {
var result = comparator.compare(a, b);
print('Comparison result: $result');
}
void main() {
// This works due to contravariance
var comparator = AnimalComparator();
compareDogs(comparator, Dog(), Dog()); // Works!
}
Contravariant Example
abstract class Event {}
class ClickEvent extends Event {}
class HoverEvent extends Event {}
abstract class EventHandler<T extends Event> {
void handle(T event);
}
// Contravariant event handler
class ClickHandler extends EventHandler<ClickEvent> {
@override
void handle(covariant ClickEvent event) {
print('Handling click: $event');
}
}
class EventProcessor {
void processEvent(EventHandler<Event> handler, Event event) {
handler.handle(event);
}
}
// Usage
void main() {
var processor = EventProcessor();
var clickHandler = ClickHandler();
// This works due to covariance
processor.processEvent(clickHandler, ClickEvent());
// But not with a different event type
// processor.processEvent(clickHandler, HoverEvent()); // Error!
}
Variance in Practice
Real-World Examples
// 1. Generic Repository
abstract class Repository<T> {
void save(T entity);
T? find(String id);
List<T> findAll();
}
class UserRepository implements Repository<User> {
@override
void save(covariant Object entity) {
// Covariant parameter
if (entity is User) {
print('Saving user: ${entity.name}');
} else {
throw ArgumentError('Expected User entity');
}
}
@override
User? find(String id) {
print('Finding user: $id');
return null;
}
@override
List<User> findAll() {
print('Finding all users');
return [];
}
}
// 2. Generic Builder
abstract class Builder<T> {
T build();
}
class UserBuilder implements Builder<User> {
final String name;
final int age;
UserBuilder(this.name, this.age);
@override
User build() {
return User(name, age);
}
}
// 3. Generic Handler
abstract class Handler<T> {
void handle(T event);
}
class LogHandler extends Handler<Event> {
@override
void handle(covariant Event event) {
print('Logging: ${event.runtimeType}');
}
}
Variance with Lists
List Variance Example
void processList(List<Object> items) {
for (var item in items) {
print('Processing: $item');
}
}
void processStringList(List<String> items) {
for (var item in items) {
print('String: $item');
}
}
// Usage
void main() {
// Covariance - List<Cat> can be used as List<Animal>
List<Cat> cats = [Cat(), Cat()];
processList(cats); // Works!
// But cannot add different types
// cats.add(Dog()); // Error: Type mismatch
// List variance with modification
List<Animal> animals = [Dog(), Cat()];
// animals.add('string'); // Error: Type mismatch
}
Variance Best Practices
Use Covariant for Parameter Override
// Good: Covariant parameter
class AnimalProcessor {
void process(Animal animal) {
animal.makeSound();
}
}
class DogProcessor extends AnimalProcessor {
@override
void process(covariant Dog animal) {
animal.makeSound();
animal.fetch();
}
}
// Bad: Incorrect covariance
class BadProcessor extends AnimalProcessor {
@override
void process(covariant String animal) {
// String is not a subtype of Animal!
}
}
Use Covariant Return Types
// Good: Covariant return type
class Factory {
Animal create() => Animal();
}
class DogFactory extends Factory {
@override
Dog create() => Dog(); // Covariant return
}
// Good: Multiple levels
class AnimalCreator {
Animal createAnimal() => Animal();
}
class MammalCreator extends AnimalCreator {
@override
Mammal createAnimal() => Mammal();
}
class DogCreator extends MammalCreator {
@override
Dog createAnimal() => Dog();
}
Common Mistakes
Incorrect Covariant Usage
Wrong:
class Container<T> {
void add(covariant T item) {
// Overuse of covariant
}
}
Correct:
class Container<T> {
void add(T item) {
// Regular generic is fine
}
}
Covariance with Wrong Type
Wrong:
class AnimalProcessor {
void process(Animal animal) {}
}
class DogProcessor extends AnimalProcessor {
@override
void process(covariant String animal) {
// String is not a subtype of Animal
}
}
Correct:
class AnimalProcessor {
void process(Animal animal) {}
}
class DogProcessor extends AnimalProcessor {
@override
void process(covariant Dog animal) {
// Dog is a subtype of Animal
}
}
Summary
Variance allows flexible type relationships in generics. Covariance allows subtypes to be used where supertypes are expected, while contravariance allows supertypes to be used where subtypes are expected.
Next Steps
Now that you understand variance, continue to:
Did You Know?
- Dart generics are invariant by default
- Covariance is specified with the
covariantkeyword - Covariance is useful for method overriding
- Contravariance is less common in Dart
- Covariance helps maintain type safety
- Collections are invariant by default
- Covariance enables polymorphic behavior with generics