Det er blevet tid til sidste indlæg i min serie om nyheder i C# 4. Denne gang skal det handle om varians eller mere specifikt ko- og kontravarians.
Inden C# 4 kom på gaden var dette noget, der blev omtalt en hel del, men i realiteten er de introducerede forbedringer af den type, hvor vi egentlig ikke behøver at kende detaljerne. Vi kan blot glæde os over, at tidligere problemfyldte konstruktioner nu fungerer som forventet.
Lad os først få de relevante definitioner på plads for varians:
Kovarians vil sige, at vi kan konvertere en ”mindre” type til en ”større” type. Eksempelvis kan vi konvertere fra Int32 til Int64.
Kontravarians vil sige, at vi kan konvertere fra en ”større” type til en ”mindre” type. C# kræver eksplicit konvertering i dette tilfælde, da vi risikerer at miste data. Konverterer vi således fra Int64 til Int32, kræver C# at vi laver et cast af værdien til Int32.
Ideen bag de introducerede features er at tillade disse konverteringer på en sikker måde.
Array er kovariant
C# har understøttet kovarians for arrays siden version 1 men desværre ikke uden problemer.
Betragt nedenstående klassehierarki.
public class Creature {
}
public class Monster : Creature {
public override string ToString() { return "Argh!!"; }
}
public class Vampire : Monster {
public override string ToString() { return "I Vant Yor Blod"; }
}
public class Zombie : Monster {
public override string ToString() { return "Braaaaiins!"; }
}
Monster er en specialisering af Creature, og Zombie og Vampire er yderligere specialiseringer af Monster. Som følge af konventionerne for arv, er enhver Zombie således et Monster og et Creature. Ligeledes er enhver Vampire et Monster og et Creature. Har vi en instans af Zombie, kan vi således tillade os at betragte den som enten Zombie, Monster eller Creature.
Opretter vi et array af Vampire, burde det således være i orden at betragte det som et array af Monster, eftersom Vampire er en specialisering af Monster. Nedenstående kode er derfor tilladt.
Vampire[] vampires = new Vampire[1]; Monster[] monsters = vampires;
Så langt, så godt. Men Zombie er jo også et Monster, så hvad sker der, hvis vi smider en Zombie i vores array?
monsters[0] = new Zombie();
Compileren tillader dette, da Zombie jo er en specialisering af Monster. Afviklingsmiljøet opdager dog, at vi nu har en Zombie i vores Vampire[] og smider en ArrayTypeMismatchException. Ups!
Problemet og løsningen
Hvorfor er ovenstående et problem? Når vi kan indsætte en instans af Zombie i et array af Vampire, kan vi komme til at referere denne via en Vampire-reference, og det er som bekendt ikke tilladt i C# og derfor smider afviklingsmiljøet en exception. Heldigvis er denne form for varians kun understøttet for array. Forsøger vi således ovenstående med List, kommer vi ikke langt.
List<Vampire> vampires = new List<Vampire>(); List<Monster> monsters = vampires; // compilefejl
Ovenstående oversætter således ikke. Men hvorfor egentlig ikke? Hvorfor kan vi ikke anskue en samling af Vampire som en samling af Monster, når vi kan betragte en enkelt instans af Vampire som en instans af Monster?
Problemet er at array tillader, at vi ændrer indholdet, så hvis vi ønsker at betragte en samling af Vampire som en samling af Monster, er vi nødt til at gøre det på en måde, så vi ikke kan ændre elementerne som ovenfor.
IEnumerable<T> tillader os at gennemløbe en samling af objekter, uden at vi kan ændre denne. Så hvis vi ændrede eksemplet fra at bruge array til at bruge IEnumerable<T>, kunne det således ud:
List<Vampire> vampires = new List<Vampire>(); IEnumerable<Monster> monsters = vampires;
Desværre virker ovenstående ikke i tidligere version af C#, for skønt vi ved, at vi ikke kan ændre indholdet via en IEnumerable<T>-reference, er compileren desværre uvidende på dette punkt. Vi mangler altså en måde at synliggøre dette over for compileren.
I .NET 4 er IEnumerable<T> og flere andre typer således blevet opdateret med denne information via en ny syntaks til understøttelse af ko- og kontravarians. Den opdaterede definition af IEnumerable<T> er som følger:
IEnumerable<out T>
out indikerer, at vores instans af T kun bliver brugt som output. Vi kan altså få lov at løbe en samling af T igennem og betragte hvert element som instanser af Ts baseklasse, fordi elementerne kun optræder som output.
På samme vis er f.eks. Action<T> blevet opdateret som følger:
Action<in T>
Her angiver in, at instanser af T kun bliver brugt som input til metoden, og dermed kan vi tillade følgende:
Action<Monster> write_monster = m => { Console.WriteLine(m); };
Action<Vampire> write_vampire = write_monster;
Eftersom Vampire er en specialisering af Monster, kan vi betragte en metode, der tager en instans af Vampire som input, som en gyldig variant af en metode, der tager en instans af Monster som input, eftersom alle instanser af Vampire også er en instans af Monster, og dermed har vi fået en kontravariant version af Action<T>.
Afrundning og næste punkt på programmet
Ko- og kontravarians er kun understøttet for interfaces og delegate-typer, og der skal naturligvis findes en gyldig referencekonvertering mellem de involverede typer, for at det kan lade sig gøre. Ydermere er ko- og kontravarians ikke understøttet for ref og out.
Der er flere detaljer omkring varians, men jeg runder min gennemgang af her. Det er ikke nødvendigt, at forstå samtlige finurligheder for at glæde sig over, at visse oplagte konstruktioner nu er understøttet.
Dermed er vi nået til enden af min gennemgang af nyheder i C# 4.
Jeg er ved at forberede en TechTalk om PFX (tag jer ikke af overskriften, der er et eller andet galt med Microsofts event-system), så de næste mange indlæg kommer til at omhandle parallel programmering og de nye muligheder i .NET 4 og VS2010.

