A duck typing kifejezéssel legtöbb esetben dinamikus típusos nyelvek esetén találkozhatunk. Ezen nyelvek hátránya, hogy nincsenek interfészek, amelyekkel viselkedést tudnánk definiálni két komponens között. Emiatt az egyetlen módja annak, hogy meggyőződjünk egy metódusban arról, hogy a paraméterként valami tényleg az amit várunk megvizsgáljuk és abduktív1 érvelés segítségével arra a következtetésre jutunk, hogy: „Ha úgy néz ki mint egy kacsa és hápog, akkor nagy valószínűséggel kacsa.”
No, de mit jelent ez a gyakorlatban és miért fontos ez C# esetén, ahol vannak interfészek?
Alapvetően azért, mert a C# fordító belső működésében épít pár helyen erre a technikára. Ez tudom, önmagában egy elég gyenge érv a technika magyarázására, mivel egy fordító elég speciális eset. A gyakorlatban C# fejlesztőként akkor találkozhatunk vele, ha dynamic kulcsszóval létrehozott változókkal dolgozunk.
Ezen változók típusa a fordítás közben nem ismert, futás időben dől el, hogy egy valami milyen típussal rendelkezik, így fordítási időben nem tudunk róla eldönteni dolgokat. Nézzünk egy példát:
using System;
namespace DuckTyping
{
internal class Kacsa
{
public void Swim()
{
Console.WriteLine("A kacsa úszik");
}
public void Fly()
{
Console.WriteLine("A kacsa repül");
}
}
internal class Balna
{
public void Swim()
{
Console.WriteLine("A bálna úszik");
}
}
internal static class Program
{
private static void Main(string[] args)
{
try
{
dynamic k = new Kacsa();
dynamic b = new Balna();
k.Swim();
k.Fly();
b.Swim();
b.Fly();
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
}
}
A fenti példában van egy Kacsa és Balna osztályunk, amelyeken megpróbáljuk meghívni a Fly és Swim metódusokat. Ahogy várható, a bálna esetén ez hibába ütközik és egy RuntimeBinderException kivételt kapunk, mert nincs ilyen metódusa. A dinamikus nyelvekben (pl. JavaScript) nyelvileg létezik lehetőség arra, hogy megnézzük, hogy egy adott osztály definiál-e egy metódust. C# esetén erre interfészeket tudnánk használni. Azonban ha tudunk interfészeket bevezetni, akkor az azt jelenti, hogy nincs is valójában szükségünk Duck typing-ra, mert pattern matching segítségével nagyon szépen kezelni tudjuk a különböző eseteket. De mi van akkor, ha nem a miénk az osztályhierarchia és az interfész bevezetés lehetősége szóba sem jöhet? Ebben az esetben a reflection-re támazkodhatunk, vagy alapozhatunk a RuntimeBinderException kivételre a kezelés során:
using Microsoft.CSharp.RuntimeBinder;
using System;
namespace DuckTyping
{
internal class Kacsa
{
public void Swim()
{
Console.WriteLine("A kacsa úszik");
}
public void Fly()
{
Console.WriteLine("A kacsa repül");
}
}
internal class Balna
{
public void Swim()
{
Console.WriteLine("A bálna úszik");
}
}
internal static class Program
{
private static bool TryCall(object @object, Action<dynamic> action)
{
try
{
action.Invoke(@object);
return true;
}
catch (RuntimeBinderException)
{
return false;
}
}
private static void Main(string[] args)
{
dynamic k = new Kacsa();
dynamic b = new Balna();
k.Swim();
k.Fly();
b.Swim();
Action<dynamic> flycall = b => b.Fly();
bool result = TryCall(b, flycall);
if (!result)
{
Console.WriteLine("A bálna nem tud repülni");
}
}
}
}
A program kimenete:
A kacsa úszik
A kacsa repül
A bálna úszik
A bálna nem tud repülni
Duck typing a nyelvben
A C#-ban ékes példa a Duck typing-ra a kollekció inicializálók működése. Ott az IEnumerable<T> implementálása és az Add() metódus megléte alapján a szükséges kódot legenerálja a fordító, de nem csak ez az egyetlen hely, ahol ez alkalmazva van.
A foreach működésénél kitárgyaltuk, hogy minden osztály bejárható ezzel a ciklussal, ami implementálja az IEnumerable vagy IEnumerable<T> interfészt. Azonban ez nem a valóság. A valóságban bármely osztályra rá tudjuk hívni a foreaach ciklust, ami rendelkezik egy GetEnumerator() metódussal és egy enumerátort ad vissza.
A GetEnumerator() lehet egyébként extension method is, így nem kell az osztályban lennie a definíciónak, így akár az alábbi kód is forduló és működőképes C# kód lehet:
foreach (int number in 20)
{
Console.WriteLine(number);
}
A megvalósítása:
using System;
using System.Collections.Generic;
using System.Linq;
namespace DuckTypingEnumerator
{
public static class Extensions
{
public static IEnumerator<int> GetEnumerator(this int limit)
{
return Enumerable.Range(0, limit).GetEnumerator();
}
}
internal static class Program
{
private static void Main(string[] args)
{
foreach (int number in 20)
{
Console.Write($"{number} ");
}
}
}
}
A program kimenete:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
A másik kevésbé ismert eset az await működéséhez köthető.
A korábbi fejezetekben arra építettünk, hogy a Task típusra tudunk várni az await kulcsszóval, de ez sem a teljes igazság. Valójában bármilyen típusra tudjuk alkalmazni az await kulcsszót, amíg a típus megfelel az alábbi követelményeknek:
- Implementálja az
INotifyCompletioninterfészt - Rendelkezik egy
booltípusúIsCompletedtulajdonsággal - Rendelkezik egy paraméter nélküli
GetResult()metódussal, aminek a visszatérési értékeobjectvagy egy tetszőlegesTtípus.
Az await esetén ezt a működést az indokolja, hogy létezik struktúra alapú ValueType is, ami nem öröklődhet az osztály alapú Task típusból, de mindkét esetben működnie kell az await utasításnak, típustól függetlenül. Persze be lehetett volna vezetni egy interfészt a két típus absztrakciójára, de egészen a .NET 8 megjelenéséig egy közös interfészen a metódus hívás költséges műveletnek számított (minden hívás előtt történt egy típus ellenőrzés), ami igencsak negatív hatással lett volna a teljesítményre egy igen teljesítmény kritikus részen, ezért inkább az ellenőrzést a fordítóban oldották meg futási idő helyett.
Szóval akár a gyakorlatban írhatunk egy ilyen forduló kódrészletet is:
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System;
namespace DuckTypingAsync
{
public static class Program
{
public static async Task Main()
{
await 10.Seconds();
}
}
public static class Extensions
{
public static TimeSpan Seconds(this int number)
{
return TimeSpan.FromSeconds(number);
}
public static TaskAwaiter GetAwaiter(this TimeSpan time)
{
return Task.Delay(time).GetAwaiter();
}
}
}
A kódban a TaskAwaiter struktúra implementájla az INotifyCompletion interfészt és ezt az osztályt alkalmazza a fordító az await működtetéséhez.
A fenti két példa esetén mindenképpen kiemelendőnek érzem azt, hogy a bár lehetőségünk megvan az ehhez hasonló kódok írására, nem biztos, hogy ez a legjobb ötlet. A Duck typing tud hasznos lenni, de csak akkor, ha nincs más lehetőségünk. Szándékosan ne abuzáljuk a nyelv lehetőségeit, mert ez rövidebb vagy hosszabb távon karbantarthatatlan kódot fog eredményezni.
-
egy abduktív érv két fogalomra utal, amelyek egymáshoz kapcsolódnak, de még így is eltérőek. Mindkettő magyarázó érvekre utal. – https://hu.thpanorama.com/articles/cultura-general/qu-es-un-argumento-abductivo-con-ejemplos.html↩