Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
La corrispondenza dei modelli consente di aggiungere funzionalità per i tipi definiti in altre librerie senza modificare tali tipi. Anche i pattern possono essere usati per creare funzionalità necessarie per l'applicazione che non sono caratteristiche fondamentali del tipo da estendere.
In questa esercitazione apprenderai a:
- Riconoscere le situazioni in cui utilizzare la corrispondenza dei modelli.
- Usare le espressioni di pattern matching per implementare la funzionalità in base ai tipi e ai valori delle proprietà.
- Combinare il matching dei pattern con altre tecniche per creare algoritmi completi.
Prerequisites
- La versione più recente .NET SDK
- editor Visual Studio Code
- Il DevKit C#
Istruzioni per l'installazione
Su Windows, usa questo file di configurazione WinGet per installare tutti i prerequisiti. Se è già installato un elemento, WinGet ignorerà questo passaggio.
- Scaricare il file e fare doppio clic per eseguirlo.
- Leggere il contratto di licenza, digitare ye selezionare Immettere quando viene richiesto di accettare.
- Se viene visualizzato un prompt di controllo dell'account utente lampeggiante nella barra delle applicazioni, consentire all'installazione di continuare.
In altre piattaforme è necessario installare ognuno di questi componenti separatamente.
- Scaricare il programma di installazione consigliato dalla pagina di download .NET SDK e fare doppio clic per eseguirlo. La pagina di download rileva la piattaforma e consiglia il programma di installazione più recente per la piattaforma.
- Scaricare il programma di installazione più recente dalla home page Visual Studio Code e fare doppio clic per eseguirlo. Questa pagina rileva anche la tua piattaforma e il collegamento dovrebbe essere corretto per il tuo sistema.
- Fare clic sul pulsante "Installa" nella pagina dell'estensione C# DevKit. Si apre Visual Studio Code e ti chiede se vuoi installare o abilitare l'estensione. Seleziona "install".
Scenari per la corrispondenza di modelli
Lo sviluppo moderno spesso include l'integrazione dei dati da più origini e la presentazione di informazioni e approfondimenti da tali dati in una singola applicazione coerente. Tu e il tuo team potreste non controllare i tipi che rappresentano i dati in ingresso.
L'approccio classico alla progettazione orientata agli oggetti richiede la creazione di tipi nell'applicazione che rappresentano ogni tipo di dato proveniente da tali origini dati multiple. L'applicazione funziona quindi con questi nuovi tipi, costruisce gerarchie di ereditarietà, crea metodi virtuali e implementa astrazioni. Queste tecniche funzionano e in alcuni casi sono i migliori strumenti. In altri casi è possibile scrivere meno codice. In alcuni casi, separare i dati dalle operazioni che li usano può rendere il codice più facile da leggere.
In questa esercitazione viene creata ed esplorata un'applicazione che accetta i dati in ingresso da diverse origini esterne per un singolo scenario. Si vede come il pattern matching fornisca un modo efficiente per consumare ed elaborare i dati in modi che non facevano parte del sistema originale.
Si consideri una grande area metropolitana che usa pedaggi e prezzi di picco per gestire il traffico. Si scrive un'applicazione che calcola i pedaggi in base al tipo di veicolo. I miglioramenti successivi incorporano i prezzi in base al numero di passeggeri del veicolo. Ulteriori miglioramenti aggiungono i prezzi in base all'ora e al giorno della settimana.
Da questa breve descrizione, è possibile tracciare rapidamente una gerarchia di oggetti per modellare questo sistema. Tuttavia, i dati provengono da più origini come altri sistemi di gestione delle registrazioni dei veicoli. Questi sistemi forniscono classi diverse per modellare i dati e tali sistemi non condividono un singolo modello a oggetti. In questa esercitazione si usano queste classi semplificate per modellare i dati del veicolo da questi sistemi esterni, come illustrato nel codice seguente:
namespace ConsumerVehicleRegistration
{
public class Car
{
public int Passengers { get; set; }
}
}
namespace CommercialRegistration
{
public class DeliveryTruck
{
public int GrossWeightClass { get; set; }
}
}
namespace LiveryRegistration
{
public class Taxi
{
public int Fares { get; set; }
}
public class Bus
{
public int Capacity { get; set; }
public int Riders { get; set; }
}
}
È possibile scaricare il codice iniziale dal repository GitHub dotnet/samples. È possibile notare che le classi del veicolo provengono da sistemi diversi e si trovano in spazi dei nomi diversi. Non è possibile usare una classe base comune diversa da System.Object.
Progetti di corrispondenza di modelli
Lo scenario usato in questa esercitazione evidenzia i tipi di problemi risolti correttamente dalla corrispondenza dei modelli:
- Gli oggetti da usare non sono in una gerarchia di oggetti che corrisponde ai propri obiettivi. È possibile usare classi che fanno parte di sistemi non correlati.
- Le funzionalità che si stanno aggiungendo non fanno parte dell'astrazione fondamentale per queste classi. Il pedaggio pagato da un veicolo cambia per i diversi tipi di veicoli, ma il pedaggio non è una funzione fondamentale del veicolo.
Quando la forma dei dati e le operazioni su tali dati non sono descritte insieme, le funzionalità dei criteri di ricerca in C# ne semplificano l'uso.
Implementare i calcoli di base per i pedaggi
Il calcolo dei pedaggi più semplice si basa solo sul tipo di veicolo:
- Un
Carcosta $2,00. - Un
Taxicosta $ 3,50. - Un
Buscosta $ 5,00. - Un
DeliveryTruckè $ 10,00.
Creare una nuova classe TollCalculator e implementare il pattern matching sul tipo di veicolo per ottenere l'importo del pedaggio. Nel codice seguente viene illustrata l'implementazione di TollCalculator.
using System;
using CommercialRegistration;
using ConsumerVehicleRegistration;
using LiveryRegistration;
namespace Calculators;
public class TollCalculator
{
public decimal CalculateToll(object vehicle) =>
vehicle switch
{
Car c => 2.00m,
Taxi t => 3.50m,
Bus b => 5.00m,
DeliveryTruck t => 10.00m,
{ } => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))
};
}
Il codice precedente usa un'switchespressione (diversa da un'switchistruzione) che testa il criterio della dichiarazione. Un'espressione switch inizia con la variabile, vehicle nel codice precedente, seguita dalla parola chiave switch. Seguono quindi tutti gli elementi switch tra parentesi graffe. L'espressione switch perfeziona ulteriormente la sintassi che racchiude l'istruzione switch. La parola chiave case viene omessa e il risultato di ogni elemento è un'espressione. Gli ultimi due rami mostrano una nuova funzionalità del linguaggio. Nella dichiarazione { }, corrisponde a qualsiasi oggetto non nullo che non corrisponda a un ramo precedente. Questo braccio rileva eventuali tipi non corretti passati a questo metodo. Il caso { } deve seguire i casi per ogni tipo di veicolo. Se l'ordine fosse invertito, il caso { } prenderebbe la precedenza. Infine, il nullcriterio costante rileva quando null viene passato a questo metodo. Il criterio null può essere l'ultimo perché gli altri criteri corrispondono solo a un oggetto non Null del tipo corretto.
È possibile testare questo codice usando il codice seguente in Program.cs:
using System;
using CommercialRegistration;
using ConsumerVehicleRegistration;
using LiveryRegistration;
using toll_calculator;
var tollCalc = new TollCalculator();
var car = new Car();
var taxi = new Taxi();
var bus = new Bus();
var truck = new DeliveryTruck();
Console.WriteLine($"The toll for a car is {tollCalc.CalculateToll(car)}");
Console.WriteLine($"The toll for a taxi is {tollCalc.CalculateToll(taxi)}");
Console.WriteLine($"The toll for a bus is {tollCalc.CalculateToll(bus)}");
Console.WriteLine($"The toll for a truck is {tollCalc.CalculateToll(truck)}");
try
{
tollCalc.CalculateToll("this will fail");
}
catch (ArgumentException e)
{
Console.WriteLine("Caught an argument exception when using the wrong type");
}
try
{
tollCalc.CalculateToll(null!);
}
catch (ArgumentNullException e)
{
Console.WriteLine("Caught an argument exception when using null");
}
Tale codice è incluso nel progetto iniziale, ma è impostato come commento. Rimuovere i commenti ed è possibile testare ciò che è stato scritto.
Si può già osservare come i criteri consentano di creare algoritmi in cui il codice e i dati sono separati. L'espressione switch testa il tipo e genera valori diversi in base ai risultati. Si tratta solo dell'inizio.
Aggiungere i prezzi in base al numero degli occupanti
L'autorità di regolazione dei pedaggi vuole incoraggiare i conducenti a viaggiare al massimo della capacità. Decidono di far pagare di più quando i veicoli hanno meno passeggeri e incoraggiano i veicoli completi offrendo prezzi più bassi:
- Automobili e taxi senza passeggeri pagano un extra di $ 0,50.
- Automobili e taxi con due passeggeri usufruiscono di uno sconto di $ 0,50.
- Automobili e taxi con tre o più passeggeri usufruiscono di uno sconto di $ 1,00.
- Gli autobus con meno del 50% dei posti occupati pagano un extra di $ 2,00.
- Gli autobus con più del 90% dei posti occupati usufruiscono di uno sconto di $ 1,00.
È possibile implementare queste regole usando un property pattern nella stessa espressione switch. Un modello di proprietà confronta un valore della proprietà con un valore costante. Il pattern di proprietà esamina le proprietà dell'oggetto una volta determinato il tipo.
Car {Passengers: 0} è un pattern ricorsivo: il modello di proprietà esterna su Car contiene un pattern costante interno che verifica il valore Passengers. Il caso singolo di Car si espande a quattro casi diversi.
vehicle switch
{
Car {Passengers: 0} => 2.00m + 0.50m,
Car {Passengers: 1} => 2.0m,
Car {Passengers: 2} => 2.0m - 0.50m,
Car => 2.00m - 1.0m,
// ...
};
I primi tre case testano il tipo come Car, quindi controllano il valore della proprietà Passengers. Se entrambe corrispondono, l'espressione viene valutata e restituisce.
È anche possibile espandere i casi per i taxi in modo simile:
vehicle switch
{
// ...
Taxi {Fares: 0} => 3.50m + 1.00m,
Taxi {Fares: 1} => 3.50m,
Taxi {Fares: 2} => 3.50m - 0.50m,
Taxi => 3.50m - 1.00m,
// ...
};
Successivamente, implementa le regole di occupazione espandendo i casi per gli autobus, come illustrato nell'esempio seguente.
vehicle switch
{
// ...
Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
Bus => 5.00m,
// ...
};
L'autorità di regolazione dei pedaggi non considera il numero di passeggeri dei furgoni, L'ammontare dei pedaggi viene invece calcolato sulla base della classe di peso dei furgoni, come indicato di seguito:
- I furgoni oltre le 5000 libbre (2268 kg) pagano un extra di $ 5,00.
- I furgoni leggeri, sotto le 3000 libbre (1360 kg), usufruiscono di uno sconto di $ 2,00.
È possibile implementare tale regola con il codice seguente:
vehicle switch
{
// ...
DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m,
DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m,
DeliveryTruck => 10.00m,
};
Il codice precedente illustra la clausola when di un braccio di switch. Utilizzare la when clausola per testare condizioni diverse dall'uguaglianza in una proprietà . Al termine, si dispone di un metodo simile al codice seguente:
vehicle switch
{
Car {Passengers: 0} => 2.00m + 0.50m,
Car {Passengers: 1} => 2.0m,
Car {Passengers: 2} => 2.0m - 0.50m,
Car => 2.00m - 1.0m,
Taxi {Fares: 0} => 3.50m + 1.00m,
Taxi {Fares: 1} => 3.50m,
Taxi {Fares: 2} => 3.50m - 0.50m,
Taxi => 3.50m - 1.00m,
Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
Bus => 5.00m,
DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m,
DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m,
DeliveryTruck => 10.00m,
{ } => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))
};
Come indicato in precedenza, questi bracci switch sono modelli ricorsivi, annidando un modello costante all'interno di un modello di proprietà.
È possibile rendere meno ripetitivo questo codice usando switch annidate. Negli esempi precedenti, sia Car che Taxi hanno quattro braccia diverse. In entrambi i casi, è possibile creare un modello di dichiarazione che si integra in un modello costante. Questa tecnica è illustrata nel codice seguente:
public decimal CalculateToll(object vehicle) =>
vehicle switch
{
Car c => c.Passengers switch
{
0 => 2.00m + 0.5m,
1 => 2.0m,
2 => 2.0m - 0.5m,
_ => 2.00m - 1.0m
},
Taxi t => t.Fares switch
{
0 => 3.50m + 1.00m,
1 => 3.50m,
2 => 3.50m - 0.50m,
_ => 3.50m - 1.00m
},
Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
Bus b => 5.00m,
DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m,
DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m,
DeliveryTruck t => 10.00m,
{ } => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))
};
Nell'esempio precedente, annidare un'espressione switch all'interno di un altro ramo switch significa che non si ripetono i rami Car e Taxi contenenti i rami figlio che testano il valore della proprietà. Questa tecnica non viene usata per le braccia Bus e DeliveryTruck perché tali braccia verificano intervalli per la proprietà, non valori discreti.
Aggiungi i prezzi per le ore di punta
Per la funzionalità finale, l'autorità dei pedaggi vuole aggiungere tariffe di punta variabili in base all'orario. Durante le ore di punta del mattino e della sera, i pedaggi sono raddoppiati. Tale regola viene applicata al traffico in una sola direzione: in entrata in città durante le ore di punta del mattino e in uscita durante quelle serali. Negli altri orari della giornata lavorativa, i pedaggi aumentano del 50%. La sera tardi e la mattina presto, i pedaggi sono ridotti del 25%. Durante il fine settimana, si paga la normale tariffa, a qualsiasi ora. È possibile usare una serie di istruzioni if e else per esprimere questa regola usando il codice seguente:
public decimal PeakTimePremiumIfElse(DateTime timeOfToll, bool inbound)
{
if ((timeOfToll.DayOfWeek == DayOfWeek.Saturday) ||
(timeOfToll.DayOfWeek == DayOfWeek.Sunday))
{
return 1.0m;
}
else
{
int hour = timeOfToll.Hour;
if (hour < 6)
{
return 0.75m;
}
else if (hour < 10)
{
if (inbound)
{
return 2.0m;
}
else
{
return 1.0m;
}
}
else if (hour < 16)
{
return 1.5m;
}
else if (hour < 20)
{
if (inbound)
{
return 1.0m;
}
else
{
return 2.0m;
}
}
else // Overnight
{
return 0.75m;
}
}
}
Il codice precedente funziona correttamente, ma non è leggibile. È necessario collegare in sequenza tutti i casi di input e le istruzioni annidate if per ragionare sul codice. Invece, userai la corrispondenza di modelli per questa funzionalità, integrandola però con altre tecniche. È possibile creare un'unica espressione di corrispondenza di pattern che consideri tutte le combinazioni di direzione, giorno della settimana e ora. Il risultato sarà un'espressione complessa, difficile da leggere e da comprendere, Risulta difficile garantire la correttezza. In alternativa, combinare questi metodi per compilare una tupla di valori che descrive in modo conciso tutti gli stati. Usare quindi la corrispondenza di modelli per calcolare un moltiplicatore per il pedaggio. La tupla contiene tre condizioni distinte:
- Il giorno è un giorno feriale o un giorno del fine settimana.
- La fascia oraria in cui il pedaggio viene riscosso.
- La direzione è verso la città o fuori dalla città.
La tabella seguente mostra le combinazioni dei valori di input e il moltiplicatore dei prezzi per le ore di punta:
| Giorno | Time | Direction | Premium |
|---|---|---|---|
| Giorno feriale | traffico del mattino | inbound | x 2,00 |
| Giorno feriale | traffico del mattino | outbound | x 1,00 |
| Giorno feriale | ore diurne | inbound | x 1,50 |
| Giorno feriale | ore diurne | outbound | x 1,50 |
| Giorno feriale | traffico serale | inbound | x 1,00 |
| Giorno feriale | traffico serale | outbound | x 2,00 |
| Giorno feriale | durante la notte | inbound | x 0,75 |
| Giorno feriale | durante la notte | outbound | x 0,75 |
| fine settimana | traffico del mattino | inbound | x 1,00 |
| fine settimana | traffico del mattino | outbound | x 1,00 |
| fine settimana | ore diurne | inbound | x 1,00 |
| fine settimana | ore diurne | outbound | x 1,00 |
| fine settimana | traffico serale | inbound | x 1,00 |
| fine settimana | traffico serale | outbound | x 1,00 |
| fine settimana | durante la notte | inbound | x 1,00 |
| fine settimana | durante la notte | outbound | x 1,00 |
Sono presenti 16 combinazioni diverse delle tre variabili. Combinando alcune delle condizioni, si semplifica l'espressione switch finale.
Il sistema che raccoglie i pedaggi usa una struttura DateTime per l'ora in cui il pedaggio è stato riscosso. Costruire metodi membri che creano le variabili dalla tabella precedente. La funzione seguente utilizza un'espressione switch con pattern matching per esprimere se DateTime rappresenta il fine settimana o un giorno feriale.
private static bool IsWeekDay(DateTime timeOfToll) =>
timeOfToll.DayOfWeek switch
{
DayOfWeek.Monday => true,
DayOfWeek.Tuesday => true,
DayOfWeek.Wednesday => true,
DayOfWeek.Thursday => true,
DayOfWeek.Friday => true,
DayOfWeek.Saturday => false,
DayOfWeek.Sunday => false
};
Questo metodo è corretto, ma è ripetitivo. È possibile semplificarlo, come illustrato nel codice seguente:
private static bool IsWeekDay(DateTime timeOfToll) =>
timeOfToll.DayOfWeek switch
{
DayOfWeek.Saturday => false,
DayOfWeek.Sunday => false,
_ => true
};
Aggiungere quindi una funzione simile per classificare l'ora in blocchi di:
private enum TimeBand
{
MorningRush,
Daytime,
EveningRush,
Overnight
}
private static TimeBand GetTimeBand(DateTime timeOfToll) =>
timeOfToll.Hour switch
{
< 6 or > 19 => TimeBand.Overnight,
< 10 => TimeBand.MorningRush,
< 16 => TimeBand.Daytime,
_ => TimeBand.EveningRush,
};
Si aggiunge un elemento enum privato per convertire ogni intervallo di tempo in un valore discreto. Il GetTimeBand metodo usa quindi modelli relazionali e modelli logici. Un criterio relazionale consente di testare un valore numerico usando <, >, <= o >=. I modelli logici combinano altri modelli: un or modello verifica se un'espressione corrisponde a uno o più modelli, un and modello verifica che un'espressione corrisponda a due modelli distinti e un not modello verifica che un'espressione non corrisponda a un modello.
Dopo aver creato questi metodi, è possibile usare un'altra switch espressione con un pattern di tupla, ovvero un pattern posizionale che corrisponde a ogni elemento di una tupla, per calcolare la maggiorazione di prezzo. È possibile creare un'espressione switch con tutti i 16 elementi:
public decimal PeakTimePremiumFull(DateTime timeOfToll, bool inbound) =>
(IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
{
(true, TimeBand.MorningRush, true) => 2.00m,
(true, TimeBand.MorningRush, false) => 1.00m,
(true, TimeBand.Daytime, true) => 1.50m,
(true, TimeBand.Daytime, false) => 1.50m,
(true, TimeBand.EveningRush, true) => 1.00m,
(true, TimeBand.EveningRush, false) => 2.00m,
(true, TimeBand.Overnight, true) => 0.75m,
(true, TimeBand.Overnight, false) => 0.75m,
(false, TimeBand.MorningRush, true) => 1.00m,
(false, TimeBand.MorningRush, false) => 1.00m,
(false, TimeBand.Daytime, true) => 1.00m,
(false, TimeBand.Daytime, false) => 1.00m,
(false, TimeBand.EveningRush, true) => 1.00m,
(false, TimeBand.EveningRush, false) => 1.00m,
(false, TimeBand.Overnight, true) => 1.00m,
(false, TimeBand.Overnight, false) => 1.00m,
};
Il codice precedente funziona, ma può essere semplificato. Tutte le otto combinazioni per il fine settimana hanno lo stesso pedaggio. È possibile sostituire tutte le otto combinazioni con la sola riga seguente:
(false, _, _) => 1.0m,
Sia traffico in entrata che quello in uscita hanno lo stesso moltiplicatore durante le ore diurne e notturne dei giorni feriali. È possibile sostituire quei quattro bracci switch con le due linee seguenti:
(true, TimeBand.Overnight, _) => 0.75m,
(true, TimeBand.Daytime, _) => 1.5m,
Dopo le due modifiche, il codice dovrebbe essere simile al seguente:
public decimal PeakTimePremium(DateTime timeOfToll, bool inbound) =>
(IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
{
(true, TimeBand.MorningRush, true) => 2.00m,
(true, TimeBand.MorningRush, false) => 1.00m,
(true, TimeBand.Daytime, _) => 1.50m,
(true, TimeBand.EveningRush, true) => 1.00m,
(true, TimeBand.EveningRush, false) => 2.00m,
(true, TimeBand.Overnight, _) => 0.75m,
(false, _, _) => 1.00m,
};
Infine, rimuovere le due fasce orarie dell'ora di punta a tariffa normale. Dopo aver rimosso questi rami, sostituisci il false con uno scarto (_) nel ramo dell'interruttore finale. Si ottiene il metodo completato seguente:
public decimal PeakTimePremium(DateTime timeOfToll, bool inbound) =>
(IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
{
(true, TimeBand.Overnight, _) => 0.75m,
(true, TimeBand.Daytime, _) => 1.5m,
(true, TimeBand.MorningRush, true) => 2.0m,
(true, TimeBand.EveningRush, false) => 2.0m,
_ => 1.0m,
};
Questo esempio illustra uno dei vantaggi della corrispondenza dei pattern: i rami dei pattern vengono valutati in ordine. Se si modifica l'ordine in modo che un ramo precedente gestisca uno dei case successivi, il compilatore avverte che il codice non è raggiungibile. Grazie alle regole del linguaggio, è stato più facile eseguire le semplificazioni precedenti con la certezza che il codice non fosse modificato.
Il pattern matching rende alcuni tipi di codice più leggibili e costituisce un'alternativa alle tecniche orientate agli oggetti quando non è possibile aggiungere codice alle classi. Nel cloud i dati e le funzionalità sono separati. La forma dei dati e le operazioni su di essi non sono necessariamente descritte insieme. In questa esercitazione i dati esistenti sono stati utilizzati in modi completamente diversi dalla funzione originale. Il pattern matching ha permesso di scrivere funzionalità che hanno eseguito l'override di tali tipi, anche se non era possibile estenderli.
Passaggi successivi
È possibile scaricare il codice completo dal repository GitHub dotnet/samples. Esplora gli schemi da solo e aggiungi questa tecnica alle attività di programmazione abituali. L'apprendimento di queste tecniche offre un altro modo per affrontare i problemi e creare nuove funzionalità.