C# Language
gränssnitt
Sök…
Implementera ett gränssnitt
Ett gränssnitt används för att säkerställa närvaron av en metod i alla klasser som "implementerar" den. Gränssnittet är definierat med sökordet interface
och en klass kan 'genomföra' den genom att lägga : InterfaceName
efter klassnamnet. En klass kan implementera flera gränssnitt genom att separera varje gränssnitt med ett komma.
: InterfaceName, ISecondInterface
public interface INoiseMaker
{
string MakeNoise();
}
public class Cat : INoiseMaker
{
public string MakeNoise()
{
return "Nyan";
}
}
public class Dog : INoiseMaker
{
public string MakeNoise()
{
return "Woof";
}
}
Eftersom de implementerar INoiseMaker
, INoiseMaker
både cat
och dog
inkludera string MakeNoise()
-metoden och kommer inte att kompilera utan den.
Implementera flera gränssnitt
public interface IAnimal
{
string Name { get; set; }
}
public interface INoiseMaker
{
string MakeNoise();
}
public class Cat : IAnimal, INoiseMaker
{
public Cat()
{
Name = "Cat";
}
public string Name { get; set; }
public string MakeNoise()
{
return "Nyan";
}
}
Explicit gränssnitt implementering
Explicit gränssnittimplementering är nödvändig när du implementerar flera gränssnitt som definierar en gemensam metod, men olika implementeringar krävs beroende på vilket gränssnitt som används för att ringa metoden (observera att du inte behöver explicita implementeringar om flera gränssnitt delar samma metod och en gemensam implementering är möjlig).
interface IChauffeur
{
string Drive();
}
interface IGolfPlayer
{
string Drive();
}
class GolfingChauffeur : IChauffeur, IGolfPlayer
{
public string Drive()
{
return "Vroom!";
}
string IGolfPlayer.Drive()
{
return "Took a swing...";
}
}
GolfingChauffeur obj = new GolfingChauffeur();
IChauffeur chauffeur = obj;
IGolfPlayer golfer = obj;
Console.WriteLine(obj.Drive()); // Vroom!
Console.WriteLine(chauffeur.Drive()); // Vroom!
Console.WriteLine(golfer.Drive()); // Took a swing...
Implementeringen kan inte kallas någon annanstans än med gränssnittet:
public class Golfer : IGolfPlayer
{
string IGolfPlayer.Drive()
{
return "Swinging hard...";
}
public void Swing()
{
Drive(); // Compiler error: No such method
}
}
På grund av detta kan det vara fördelaktigt att sätta komplex implementeringskod för ett uttryckligt implementerat gränssnitt i en separat, privat metod.
En explicit gränssnittimplementering kan naturligtvis endast användas för metoder som faktiskt finns för det gränssnittet:
public class ProGolfer : IGolfPlayer
{
string IGolfPlayer.Swear() // Error
{
return "The ball is in the pit";
}
}
På liknande sätt orsakar man också ett felaktigt gränssnittsimplementering utan att förklara det gränssnittet i klassen.
Ledtråd:
Implementering av gränssnitt uttryckligen kan också användas för att undvika död kod. När en metod inte längre behövs och tas bort från gränssnittet klagar kompilatorn över varje fortfarande existerande implementering.
Notera:
Programmerare förväntar sig att kontraktet ska vara detsamma oavsett typ av sammanhang och uttrycklig implementering bör inte avslöja olika beteenden när de kallas. Så till skillnad från exemplet ovan IGolfPlayer.Drive
och Drive
göra samma sak när det är möjligt.
Varför vi använder gränssnitt
Ett gränssnitt är en definition av ett kontrakt mellan användaren av gränssnittet och klassen som implementerar det. Ett sätt att tänka på ett gränssnitt är som en förklaring att ett objekt kan utföra vissa funktioner.
Låt oss säga att vi definierar ett gränssnitt IShape
att representera olika typer av former, vi förväntar oss att en form ska ha ett område, så vi kommer att definiera en metod för att tvinga gränssnittets implementationer att returnera sitt område:
public interface IShape
{
double ComputeArea();
}
Låt oss att vi har följande två former: en Rectangle
och en Circle
public class Rectangle : IShape
{
private double length;
private double width;
public Rectangle(double length, double width)
{
this.length = length;
this.width = width;
}
public double ComputeArea()
{
return length * width;
}
}
public class Circle : IShape
{
private double radius;
public Circle(double radius)
{
this.radius = radius;
}
public double ComputeArea()
{
return Math.Pow(radius, 2.0) * Math.PI;
}
}
Var och en av dem har sin egen definition av sitt område, men båda är former. Så det är bara logiskt att se dem som IShape
i vårt program:
private static void Main(string[] args)
{
var shapes = new List<IShape>() { new Rectangle(5, 10), new Circle(5) };
ComputeArea(shapes);
Console.ReadKey();
}
private static void ComputeArea(IEnumerable<IShape> shapes)
{
foreach (shape in shapes)
{
Console.WriteLine("Area: {0:N}, shape.ComputeArea());
}
}
// Output:
// Area : 50.00
// Area : 78.54
Grunderna i gränssnittet
Ett gränssnitt är funktion som kallas ett "kontrakt" med funktionalitet. Det betyder att den deklarerar egenskaper och metoder men att den inte implementerar dem.
Så till skillnad från klasserna gränssnitt:
- Kan inte instanseras
- Kan inte ha någon funktion
- Kan bara innehålla metoder * (Egenskaper och händelser är metoder internt)
- Ärva ett gränssnitt kallas "Implementing"
- Du kan ärva från en klass, men du kan "implementera" flera gränssnitt
public interface ICanDoThis{
void TheThingICanDo();
int SomeValueProperty { get; set; }
}
Saker att märka:
- Prefixet "Jag" är ett namnkonvention som används för gränssnitt.
- Funktionskroppen ersätts med en semikolon ";".
- Egenskaper är också tillåtna eftersom de internt också är metoder
public class MyClass : ICanDoThis {
public void TheThingICanDo(){
// do the thing
}
public int SomeValueProperty { get; set; }
public int SomeValueNotImplemtingAnything { get; set; }
}
.
ICanDoThis obj = new MyClass();
// ok
obj.TheThingICanDo();
// ok
obj.SomeValueProperty = 5;
// Error, this member doesn't exist in the interface
obj.SomeValueNotImplemtingAnything = 5;
// in order to access the property in the class you must "down cast" it
((MyClass)obj).SomeValueNotImplemtingAnything = 5; // ok
Detta är särskilt användbart när du arbetar med UI-ramar som WinForms eller WPF eftersom det är obligatoriskt att ärva från en basklass för att skapa användarkontroll och du förlorar förmågan att skapa abstraktion över olika kontrolltyper. Ett exempel? Kommer upp:
public class MyTextBlock : TextBlock {
public void SetText(string str){
this.Text = str;
}
}
public class MyButton : Button {
public void SetText(string str){
this.Content = str;
}
}
Problemet som föreslås är att båda innehåller något begrepp "Text" men egendomsnamnen skiljer sig åt. Och du kan inte skapa en abstrakt basklass eftersom de har en obligatorisk arv till två olika klasser. Ett gränssnitt kan lindra det
public interface ITextControl{
void SetText(string str);
}
public class MyTextBlock : TextBlock, ITextControl {
public void SetText(string str){
this.Text = str;
}
}
public class MyButton : Button, ITextControl {
public void SetText(string str){
this.Content = str;
}
public int Clicks { get; set; }
}
Nu är MyButton och MyTextBlock utbytbara.
var controls = new List<ITextControls>{
new MyTextBlock(),
new MyButton()
};
foreach(var ctrl in controls){
ctrl.SetText("This text will be applied to both controls despite them being different");
// Compiler Error, no such member in interface
ctrl.Clicks = 0;
// Runtime Error because 1 class is in fact not a button which makes this cast invalid
((MyButton)ctrl).Clicks = 0;
/* the solution is to check the type first.
This is usually considered bad practice since
it's a symptom of poor abstraction */
var button = ctrl as MyButton;
if(button != null)
button.Clicks = 0; // no errors
}
"Dölja" medlemmar med uttrycklig implementering
Hatar du det inte när gränssnitt förorenar din klass med för många medlemmar som du inte ens bryr dig om? Jag fick en lösning! Explicit implementeringar
public interface IMessageService {
void OnMessageRecieve();
void SendMessage();
string Result { get; set; }
int Encoding { get; set; }
// yadda yadda
}
Normalt skulle du implementera klassen så här.
public class MyObjectWithMessages : IMessageService {
public void OnMessageRecieve(){
}
public void SendMessage(){
}
public string Result { get; set; }
public int Encoding { get; set; }
}
Varje medlem är offentlig.
var obj = new MyObjectWithMessages();
// why would i want to call this function?
obj.OnMessageRecieve();
Svar: Det gör jag inte. Så varken ska det förklaras offentligt utan helt enkelt att förklara medlemmarna som privata kommer att göra kompilatorn ett fel
Lösningen är att använda uttrycklig implementering:
public class MyObjectWithMessages : IMessageService{
void IMessageService.OnMessageRecieve() {
}
void IMessageService.SendMessage() {
}
string IMessageService.Result { get; set; }
int IMessageService.Encoding { get; set; }
}
Så nu har du implementerat medlemmarna efter behov och de utsätter inte alla medlemmar som offentliga.
var obj = new MyObjectWithMessages();
/* error member does not exist on type MyObjectWithMessages.
* We've succesfully made it "private" */
obj.OnMessageRecieve();
Om du på allvar fortfarande vill ha åtkomst till medlemmen, trots att det uttryckligen implementeras, behöver du bara kasta objektet till gränssnittet och du är bra att gå.
((IMessageService)obj).OnMessageRecieve();
IComparable som ett exempel på implementering av ett gränssnitt
Gränssnitt kan verka abstrakt tills du verkar vara dem i praktiken. IComparable
och IComparable<T>
är bra exempel på varför gränssnitt kan vara till hjälp för oss.
Låt oss säga att i ett program för en onlinebutik har vi en mängd olika objekt du kan köpa. Varje objekt har ett namn, ett ID-nummer och ett pris.
public class Item {
public string name; // though public variables are generally bad practice,
public int idNumber; // to keep this example simple we will use them instead
public decimal price; // of a property.
// body omitted for brevity
}
Vi har våra Item
lagrade i en List<Item>
, och i vårt program någonstans vill vi sortera vår lista efter ID-nummer från minsta till största. I stället för att skriva vår egen sorteringsalgoritm kan vi istället använda den Sort()
-metoden som List<T>
redan har. Men eftersom vår Item
är just nu, finns det inget sätt för List<T>
att förstå vilken ordning listan ska sorteras på. Här är det IComparable
gränssnittet kommer in.
För att korrekt implementera CompareTo
metoden, bör CompareTo
returnera ett positivt tal om parametern är "mindre än" den aktuella, noll om de är lika, och ett negativt tal om parametern är "större än".
Item apple = new Item();
apple.idNumber = 15;
Item banana = new Item();
banana.idNumber = 4;
Item cow = new Item();
cow.idNumber = 15;
Item diamond = new Item();
diamond.idNumber = 18;
Console.WriteLine(apple.CompareTo(banana)); // 11
Console.WriteLine(apple.CompareTo(cow)); // 0
Console.WriteLine(apple.CompareTo(diamond)); // -3
Här är exemplet Item
implementering av gränssnittet:
public class Item : IComparable<Item> {
private string name;
private int idNumber;
private decimal price;
public int CompareTo(Item otherItem) {
return (this.idNumber - otherItem.idNumber);
}
// rest of code omitted for brevity
}
På en ytnivå ger CompareTo
metoden i vårt objekt helt enkelt skillnaden i deras ID-nummer, men vad gör ovanstående i praktiken?
Nu, när vi kallar Sort()
på en List<Item>
objekt, List
automatiskt kallar Item
är CompareTo
metod när den behöver för att avgöra vad för att sätta föremål i. Dessutom, förutom List<T>
, andra föremål som behöver förmågan att jämföra två objekt kommer att fungera med Item
eftersom vi har definierat möjligheten för två olika Item
att jämföras med varandra.