Una guida definitiva alle modifiche rivoluzionarie delle API in .NET
-
12-09-2019 - |
Domanda
Vorrei raccogliere quante più informazioni possibili sul controllo delle versioni dell'API in .NET/CLR e in particolare su come le modifiche dell'API interrompono o meno le applicazioni client.Per prima cosa definiamo alcuni termini:
Modifica dell'API - una modifica nella definizione pubblicamente visibile di un tipo, inclusi i suoi membri pubblici.Ciò include la modifica dei nomi dei tipi e dei membri, la modifica del tipo di base di un tipo, l'aggiunta/rimozione di interfacce dall'elenco delle interfacce implementate di un tipo, l'aggiunta/rimozione di membri (inclusi gli sovraccarichi), la modifica della visibilità dei membri, la ridenominazione di parametri di metodo e tipo, l'aggiunta di valori predefiniti per i parametri del metodo, aggiunta/rimozione di attributi su tipi e membri e aggiunta/rimozione di parametri di tipo generico su tipi e membri (mi sono perso qualcosa?).Ciò non include eventuali modifiche agli organi membri o eventuali modifiche ai membri privati (ad es.non prendiamo in considerazione la Riflessione).
Rottura a livello binario - una modifica dell'API che comporta il mancato caricamento degli assembly client compilati rispetto alla versione precedente dell'API con la nuova versione.Esempio:cambiando la firma del metodo, anche se permette di essere chiamato nello stesso modo di prima (cioè:void per restituire sovraccarichi di valori predefiniti di tipo/parametro).
Interruzione a livello di origine - una modifica dell'API che fa sì che il codice esistente scritto per essere compilato con la versione precedente dell'API potrebbe non essere compilato con la nuova versione.Tuttavia, gli assembly client già compilati funzionano come prima.Esempio:aggiunta di un nuovo sovraccarico che può provocare ambiguità nelle chiamate ai metodi che in precedenza non erano ambigue.
Modifica della semantica silenziosa a livello di origine - una modifica dell'API che comporta un codice esistente scritto per essere compilato con una versione precedente dell'API che ne modifica silenziosamente la semantica, ad es.chiamando un metodo diverso.Il codice dovrebbe tuttavia continuare a essere compilato senza avvisi/errori e gli assembly precedentemente compilati dovrebbero funzionare come prima.Esempio:implementare una nuova interfaccia su una classe esistente che comporta la scelta di un sovraccarico diverso durante la risoluzione dell'overload.
L'obiettivo finale è catalogare il maggior numero possibile di modifiche API semantiche di rottura e silenziose e descrivere l'effetto esatto della rottura e quali lingue ne sono interessate e quali non ne sono interessate.Per approfondire quest'ultimo:mentre alcuni cambiamenti riguardano universalmente tutte le lingue (ad es.l'aggiunta di un nuovo membro a un'interfaccia interromperà le implementazioni di quell'interfaccia in qualsiasi linguaggio), alcuni richiedono che una semantica linguistica molto specifica entri in gioco per ottenere una pausa.Ciò in genere comporta l'overload del metodo e, in generale, qualsiasi cosa abbia a che fare con le conversioni di tipo implicite.Non sembra esserci alcun modo per definire il "minimo comune denominatore" qui anche per i linguaggi conformi a CLS (cioèquelli conformi almeno alle regole del "consumatore CLS" come definito nelle specifiche CLI) - anche se apprezzerò se qualcuno mi corregge perché ho torto qui - quindi questo dovrà andare lingua per lingua.Quelli di maggior interesse sono naturalmente quelli forniti immediatamente con .NET:Do#, Verb e Fa#;ma sono rilevanti anche altri, come IronPython, IronRuby, Delphi Prism ecc.Più è un caso limite, più sarà interessante: cose come la rimozione di membri sono abbastanza evidenti, ma le sottili interazioni tra ad es.l'overload del metodo, i parametri opzionali/predefiniti, l'inferenza del tipo lambda e gli operatori di conversione a volte possono essere molto sorprendenti.
Alcuni esempi per avviare questo processo:
Aggiunta di nuovi sovraccarichi di metodo
Tipo:interruzione a livello di sorgente
Lingue interessate:Do#, Verb, Fa#
API prima della modifica:
public class Foo
{
public void Bar(IEnumerable x);
}
API dopo la modifica:
public class Foo
{
public void Bar(IEnumerable x);
public void Bar(ICloneable x);
}
Esempio di codice client funzionante prima della modifica e interrotto dopo:
new Foo().Bar(new int[0]);
Aggiunta di nuovi sovraccarichi dell'operatore di conversione implicita
Tipo:interruzione a livello di sorgente.
Lingue interessate:C#, Verb
Lingue non interessate:F#
API prima della modifica:
public class Foo
{
public static implicit operator int ();
}
API dopo la modifica:
public class Foo
{
public static implicit operator int ();
public static implicit operator float ();
}
Esempio di codice client funzionante prima della modifica e interrotto dopo:
void Bar(int x);
void Bar(float x);
Bar(new Foo());
Appunti:F# non è danneggiato, perché non ha alcun supporto a livello di linguaggio per gli operatori sovraccarichi, né espliciti né impliciti: entrambi devono essere chiamati direttamente come op_Explicit
E op_Implicit
metodi.
Aggiunta di nuovi metodi di istanza
Tipo:modifica della semantica silenziosa a livello di sorgente.
Lingue interessate:C#, Verb
Lingue non interessate:F#
API prima della modifica:
public class Foo
{
}
API dopo la modifica:
public class Foo
{
public void Bar();
}
Codice client di esempio che subisce una modifica silenziosa della semantica:
public static class FooExtensions
{
public void Bar(this Foo foo);
}
new Foo().Bar();
Appunti:F# non è danneggiato perché non dispone del supporto a livello di linguaggio per ExtensionMethodAttribute
, e richiede che i metodi di estensione CLS vengano chiamati come metodi statici.
Soluzione
Modifica della firma di un metodo
Tipo:Rottura a livello binario
Lingue interessate:C# (VB e F# molto probabilmente, ma non testato)
API prima della modifica
public static class Foo
{
public static void bar(int i);
}
API dopo la modifica
public static class Foo
{
public static bool bar(int i);
}
Esempio di codice client funzionante prima della modifica
Foo.bar(13);
Altri suggerimenti
Aggiunta di un parametro con un valore predefinito.
Tipo di pausa:Rottura a livello binario
Anche se il codice sorgente chiamante non necessita di modifiche, deve comunque essere ricompilato (proprio come quando si aggiunge un parametro normale).
Questo perché C# compila i valori predefiniti dei parametri direttamente nell'assembly chiamante.Significa che se non ricompili, otterrai una MissingMethodException perché il vecchio assembly tenta di chiamare un metodo con meno argomenti.
API prima della modifica
public void Foo(int a) { }
API dopo la modifica
public void Foo(int a, string b = null) { }
Codice client di esempio che viene successivamente danneggiato
Foo(5);
Il codice client deve essere ricompilato Foo(5, null)
a livello di bytecode.L'assembly chiamato conterrà solo Foo(int, string)
, non Foo(int)
.Questo perché i valori dei parametri predefiniti sono puramente una caratteristica del linguaggio, il runtime .Net non ne sa nulla.(Questo spiega anche perché i valori predefiniti devono essere costanti in fase di compilazione in C#).
Questo non era affatto ovvio quando l'ho scoperto, soprattutto alla luce della differenza con la stessa situazione per le interfacce.Non è affatto una rottura, ma è abbastanza sorprendente che ho deciso di includerlo:
Refactoring dei membri della classe in una classe base
Tipo:non una pausa!
Lingue interessate:nessuno (cioènessuno è rotto)
API prima della modifica:
class Foo
{
public virtual void Bar() {}
public virtual void Baz() {}
}
API dopo la modifica:
class FooBase
{
public virtual void Bar() {}
}
class Foo : FooBase
{
public virtual void Baz() {}
}
Codice di esempio che continua a funzionare durante la modifica (anche se mi aspettavo che si rompesse):
// C++/CLI
ref class Derived : Foo
{
public virtual void Baz() {{
// Explicit override
public virtual void BarOverride() = Foo::Bar {}
};
Appunti:
C++/CLI è l'unico linguaggio .NET che dispone di un costrutto analogo all'implementazione esplicita dell'interfaccia per i membri della classe base virtuale: "override esplicito".Mi aspettavo pienamente che ciò provocasse lo stesso tipo di rottura di quando si spostano i membri dell'interfaccia su un'interfaccia di base (poiché IL generato per l'override esplicito è lo stesso dell'implementazione esplicita).Con mia sorpresa, non è così, anche se IL generato lo specifica ancora BarOverride
sovrascrive Foo::Bar
piuttosto che FooBase::Bar
, il caricatore di assemblaggi è abbastanza intelligente da sostituirne correttamente uno con l'altro senza alcuna lamentela - a quanto pare, il fatto che Foo
è una classe è ciò che fa la differenza.Vai a capire...
Questo è un caso speciale forse non così ovvio di "aggiunta/rimozione di membri dell'interfaccia", e ho pensato che meritasse una voce a parte alla luce di un altro caso che pubblicherò di seguito.COSÌ:
Refactoring dei membri dell'interfaccia in un'interfaccia di base
Tipo:interruzioni sia a livello sorgente che binario
Lingue interessate:C#, VB, C++/CLI, F# (per interruzione del codice sorgente;quello binario influisce naturalmente su qualsiasi lingua)
API prima della modifica:
interface IFoo
{
void Bar();
void Baz();
}
API dopo la modifica:
interface IFooBase
{
void Bar();
}
interface IFoo : IFooBase
{
void Baz();
}
Codice client di esempio interrotto da una modifica a livello di origine:
class Foo : IFoo
{
void IFoo.Bar() { ... }
void IFoo.Baz() { ... }
}
Codice client di esempio interrotto da una modifica a livello binario;
(new Foo()).Bar();
Appunti:
Per l'interruzione del livello di origine, il problema è che C#, VB e C++/CLI richiedono tutti esatto nome dell'interfaccia nella dichiarazione di implementazione del membro dell'interfaccia;quindi, se il membro viene spostato su un'interfaccia di base, il codice non verrà più compilato.
L'interruzione binaria è dovuta al fatto che i metodi di interfaccia sono completamente qualificati nell'IL generato per implementazioni esplicite e anche il nome dell'interfaccia deve essere esatto.
Implementazione implicita ove disponibile (ad es.C# e C++/CLI, ma non VB) funzioneranno correttamente sia a livello sorgente che binario.Anche le chiamate ai metodi non si interrompono.
Riordinamento dei valori enumerati
Tipo di pausa: Modifica della semantica silenziosa a livello di origine/binario
Lingue interessate:Tutto
Il riordinamento dei valori enumerati manterrà la compatibilità a livello di origine poiché i valori letterali hanno lo stesso nome, ma i loro indici ordinali verranno aggiornati, il che può causare alcuni tipi di interruzioni silenziose a livello di origine.
Ancora peggiori sono le interruzioni silenziose a livello binario che possono essere introdotte se il codice client non viene ricompilato rispetto alla nuova versione dell'API.I valori enum sono costanti in fase di compilazione e come tali qualsiasi utilizzo viene inserito nell'IL dell'assembly client.Questo caso può essere particolarmente difficile da individuare a volte.
API prima della modifica
public enum Foo
{
Bar,
Baz
}
API dopo la modifica
public enum Foo
{
Baz,
Bar
}
Codice client di esempio che funziona ma successivamente si rompe:
Foo.Bar < Foo.Baz
Questa è davvero una cosa molto rara nella pratica, ma comunque sorprendente quando accade.
Aggiunta di nuovi membri non sovraccaricati
Tipo:interruzione del livello di origine o cambiamento silenzioso della semantica.
Lingue interessate:C#, Verb
Lingue non interessate:F#, C++/CLI
API prima della modifica:
public class Foo
{
}
API dopo la modifica:
public class Foo
{
public void Frob() {}
}
Codice client di esempio danneggiato dalla modifica:
class Bar
{
public void Frob() {}
}
class Program
{
static void Qux(Action<Foo> a)
{
}
static void Qux(Action<Bar> a)
{
}
static void Main()
{
Qux(x => x.Frob());
}
}
Appunti:
Il problema qui è causato dall'inferenza del tipo lambda in C# e VB in presenza della risoluzione dell'overload.Qui viene impiegata una forma limitata di tipizzazione duck per rompere i legami in cui più di un tipo corrisponde, controllando se il corpo della lambda ha senso per un dato tipo: se solo un tipo risulta in un corpo compilabile, quello viene scelto.
Il pericolo qui è che il codice client possa avere un gruppo di metodi sovraccarico in cui alcuni metodi accettano argomenti dei propri tipi e altri accettano argomenti dei tipi esposti dalla libreria.Se uno qualsiasi dei suoi codici si basa quindi sull'algoritmo di inferenza del tipo per determinare il metodo corretto basato esclusivamente sulla presenza o assenza di membri, l'aggiunta di un nuovo membro a uno dei tuoi tipi con lo stesso nome di uno dei tipi del client può potenzialmente generare un'inferenza disattivato, con conseguente ambiguità durante la risoluzione dell'overload.
Nota che tipi Foo
E Bar
in questo esempio non sono imparentati in alcun modo, né per eredità né altro.Il semplice utilizzo di essi in un singolo gruppo di metodi è sufficiente per attivare ciò e, se ciò si verifica nel codice client, non ne hai alcun controllo.
Il codice di esempio riportato sopra dimostra una situazione più semplice in cui si tratta di un'interruzione a livello di origine (ad esempiorisultati degli errori del compilatore).Tuttavia, questo può anche trattarsi di un cambiamento semantico silenzioso, se l'overload scelto tramite inferenza avesse altri argomenti che altrimenti lo avrebbero classificato di seguito (ad es.argomenti facoltativi con valori predefiniti o mancata corrispondenza del tipo tra l'argomento dichiarato e quello effettivo che richiede una conversione implicita).In tale scenario, la risoluzione dell'overload non fallirà più, ma un diverso sovraccarico verrà selezionato silenziosamente dal compilatore.In pratica, tuttavia, è molto difficile imbattersi in questo caso senza costruire attentamente le firme del metodo per provocarlo deliberatamente.
Convertire un'implementazione dell'interfaccia implicita in una esplicita.
Tipo di pausa:Sorgente e binario
Lingue interessate:Tutto
Questa è in realtà solo una variante della modifica dell'accessibilità di un metodo: è solo un po' più sottile poiché è facile trascurare il fatto che non tutti gli accessi ai metodi di un'interfaccia avvengono necessariamente tramite un riferimento al tipo di interfaccia.
API prima della modifica:
public class Foo : IEnumerable
{
public IEnumerator GetEnumerator();
}
API dopo la modifica:
public class Foo : IEnumerable
{
IEnumerator IEnumerable.GetEnumerator();
}
Codice client di esempio che funziona prima della modifica e viene interrotto in seguito:
new Foo().GetEnumerator(); // fails because GetEnumerator() is no longer public
Convertire un'implementazione esplicita dell'interfaccia in una implicita.
Tipo di pausa:Fonte
Lingue interessate:Tutto
Il refactoring di un'implementazione di interfaccia esplicita in una implicita è più sottile nel modo in cui può interrompere un'API.A prima vista sembrerebbe che questo dovrebbe essere relativamente sicuro, tuttavia, se combinato con l’ereditarietà, può causare problemi.
API prima della modifica:
public class Foo : IEnumerable
{
IEnumerator IEnumerable.GetEnumerator() { yield return "Foo"; }
}
API dopo la modifica:
public class Foo : IEnumerable
{
public IEnumerator GetEnumerator() { yield return "Foo"; }
}
Codice client di esempio che funziona prima della modifica e viene interrotto in seguito:
class Bar : Foo, IEnumerable
{
IEnumerator IEnumerable.GetEnumerator() // silently hides base instance
{ yield return "Bar"; }
}
foreach( var x in new Bar() )
Console.WriteLine(x); // originally output "Bar", now outputs "Foo"
Modifica di un campo in una proprietà
Tipo di pausa:API
Lingue interessate:Visual Basic e C#*
Informazioni:Quando si modifica un campo normale o una variabile in una proprietà in Visual Basic, qualsiasi codice esterno che faccia riferimento in qualche modo a quel membro dovrà essere ricompilato.
API prima della modifica:
Public Class Foo
Public Shared Bar As String = ""
End Class
API dopo la modifica:
Public Class Foo
Private Shared _Bar As String = ""
Public Shared Property Bar As String
Get
Return _Bar
End Get
Set(value As String)
_Bar = value
End Set
End Property
End Class
Codice client di esempio che funziona ma successivamente si rompe:
Foo.Bar = "foobar"
Aggiunta dello spazio dei nomi
Interruzione a livello di sorgente/Modifica della semantica silenziosa a livello di sorgente
A causa del modo in cui funziona la risoluzione dello spazio dei nomi in vb.Net, l'aggiunta di uno spazio dei nomi a una libreria può causare la mancata compilazione del codice Visual Basic compilato con una versione precedente dell'API con una nuova versione.
Codice cliente di esempio:
Imports System
Imports Api.SomeNamespace
Public Class Foo
Public Sub Bar()
Dim dr As Data.DataRow
End Sub
End Class
Se una nuova versione dell'API aggiunge lo spazio dei nomi Api.SomeNamespace.Data
, il codice precedente non verrà compilato.
Diventa più complicato con le importazioni di spazi dei nomi a livello di progetto.Se Imports System
è omesso dal codice precedente, ma il file System
namespace viene importato a livello di progetto, il codice potrebbe comunque generare un errore.
Tuttavia, se l'Api include una classe DataRow
nel suo Api.SomeNamespace.Data
namespace, il codice verrà compilato ma dr
sarà un esempio di System.Data.DataRow
quando compilato con la vecchia versione dell'API e Api.SomeNamespace.Data.DataRow
quando compilato con la nuova versione dell'API.
Rinominazione degli argomenti
Interruzione a livello di origine
La modifica dei nomi degli argomenti è una modifica sostanziale in vb.net dalla versione 7(?) (.Net versione 1?) e c#.net dalla versione 4 (.Net versione 4).
API prima della modifica:
namespace SomeNamespace {
public class Foo {
public static void Bar(string x) {
...
}
}
}
API dopo la modifica:
namespace SomeNamespace {
public class Foo {
public static void Bar(string y) {
...
}
}
}
Codice cliente di esempio:
Api.SomeNamespace.Foo.Bar(x:"hi"); //C#
Api.SomeNamespace.Foo.Bar(x:="hi") 'VB
Parametri di rif
Interruzione a livello di origine
L'aggiunta di un override del metodo con la stessa firma, tranne per il fatto che un parametro viene passato per riferimento anziché per valore, farà sì che l'origine vb che fa riferimento all'API non sia in grado di risolvere la funzione.Visual Basic non ha modo (?) di differenziare questi metodi nel punto di chiamata a meno che non abbiano nomi di argomenti diversi, quindi tale modifica potrebbe rendere inutilizzabili entrambi i membri dal codice vb.
API prima della modifica:
namespace SomeNamespace {
public class Foo {
public static void Bar(string x) {
...
}
}
}
API dopo la modifica:
namespace SomeNamespace {
public class Foo {
public static void Bar(string x) {
...
}
public static void Bar(ref string x) {
...
}
}
}
Codice cliente di esempio:
Api.SomeNamespace.Foo.Bar(str)
Campo per la modifica della proprietà
Rottura a livello binario/Rottura a livello di origine
Oltre all'ovvia interruzione a livello binario, ciò può causare un'interruzione a livello di origine se il membro viene passato a un metodo per riferimento.
API prima della modifica:
namespace SomeNamespace {
public class Foo {
public int Bar;
}
}
API dopo la modifica:
namespace SomeNamespace {
public class Foo {
public int Bar { get; set; }
}
}
Codice cliente di esempio:
FooBar(ref Api.SomeNamespace.Foo.Bar);
Modifica dell'API:
- Aggiunta dell'attributo [Obsoleto] (lo hai più o meno coperto menzionando gli attributi;tuttavia, questa può essere una modifica sostanziale quando si utilizza l'avviso come errore.)
Rottura a livello binario:
- Spostamento di un tipo da un assembly a un altro
- Modifica dello spazio dei nomi di un tipo
- Aggiunta di un tipo di classe base da un altro assembly.
Aggiunta di un nuovo membro (protetto da eventi) che utilizza un tipo di un altro assembly (Class2) come vincolo di argomento del modello.
protected void Something<T>() where T : Class2 { }
Modifica di una classe figlio (Class3) per derivare da un tipo in un altro assembly quando la classe viene utilizzata come argomento modello per questa classe.
protected class Class3 : Class2 { } protected void Something<T>() where T : Class3 { }
Modifica della semantica silenziosa a livello di origine:
- Aggiunta/rimozione/modifica delle sostituzioni di Equals(), GetHashCode() o ToString()
(non sono sicuro di dove si adattino)
Modifiche alla distribuzione:
- Aggiunta/rimozione di dipendenze/riferimenti
- Aggiornamento delle dipendenze alle versioni più recenti
- Modifica della "piattaforma di destinazione" tra x86, Itanium, x64 o anycpu
- Creazione/test su un'installazione di framework diversa (ad es.l'installazione di 3.5 su un box .Net 2.0 consente chiamate API che richiedono quindi .Net 2.0 SP2)
Modifiche al bootstrap/configurazione:
- Aggiunta/rimozione/modifica delle opzioni di configurazione personalizzate (ad es.Impostazioni app.config)
- Con l'uso intensivo di IoC/DI nelle applicazioni odierne, è necessario riconfigurare e/o modificare il codice di bootstrap per il codice dipendente dal DI.
Aggiornamento:
Scusa, non avevo capito che l'unico motivo per cui questo non funzionava per me era che li usavo nei vincoli del modello.
Aggiunta di metodi di sovraccarico per eliminare l'utilizzo dei parametri predefiniti
Tipo di pausa: Modifica della semantica silenziosa a livello di sorgente
Poiché il compilatore trasforma le chiamate al metodo con valori di parametro predefiniti mancanti in una chiamata esplicita con il valore predefinito sul lato chiamante, viene fornita la compatibilità per il codice compilato esistente;verrà trovato un metodo con la firma corretta per tutto il codice precedentemente compilato.
D'altro canto, le chiamate senza l'utilizzo di parametri facoltativi vengono ora compilate come una chiamata al nuovo metodo a cui manca il parametro facoltativo.Tutto funziona ancora correttamente, ma se il codice chiamato risiede in un altro assembly, il codice appena compilato che lo chiama ora dipende dalla nuova versione di questo assembly.La distribuzione di assembly che chiamano il codice sottoposto a refactoring senza distribuire anche l'assembly in cui risiede il codice sottoposto a refactoring genera eccezioni "metodo non trovato".
API prima della modifica
public int MyMethod(int mandatoryParameter, int optionalParameter = 0)
{
return mandatoryParameter + optionalParameter;
}
API dopo la modifica
public int MyMethod(int mandatoryParameter, int optionalParameter)
{
return mandatoryParameter + optionalParameter;
}
public int MyMethod(int mandatoryParameter)
{
return MyMethod(mandatoryParameter, 0);
}
Codice di esempio che funzionerà ancora
public int CodeNotDependentToNewVersion()
{
return MyMethod(5, 6);
}
Codice di esempio che ora dipende dalla nuova versione durante la compilazione
public int CodeDependentToNewVersion()
{
return MyMethod(5);
}
Rinominare un'interfaccia
Tipo di pausa:Fonte e Binario
Lingue interessate:Molto probabilmente tutti, testati in C#.
API prima della modifica:
public interface IFoo
{
void Test();
}
public class Bar
{
IFoo GetFoo() { return new Foo(); }
}
API dopo la modifica:
public interface IFooNew // Of the exact same definition as the (old) IFoo
{
void Test();
}
public class Bar
{
IFooNew GetFoo() { return new Foo(); }
}
Codice client di esempio che funziona ma successivamente si rompe:
new Bar().GetFoo().Test(); // Binary only break
IFoo foo = new Bar().GetFoo(); // Source and binary break
Metodo di overload con un parametro di tipo nullable
Tipo: Interruzione a livello di origine
Lingue interessate: C#, Verb
API prima di una modifica:
public class Foo
{
public void Bar(string param);
}
API dopo la modifica:
public class Foo
{
public void Bar(string param);
public void Bar(int? param);
}
Esempio di codice client funzionante prima della modifica e interrotto dopo:
new Foo().Bar(null);
Eccezione:La chiamata è ambigua tra i seguenti metodi o proprietà.
Promozione a Metodo di Estensione
Tipo:interruzione a livello di sorgente
Lingue interessate:C# v6 e versioni successive (forse altri?)
API prima della modifica:
public static class Foo
{
public static void Bar(string x);
}
API dopo la modifica:
public static class Foo
{
public void Bar(this string x);
}
Esempio di codice client funzionante prima della modifica e interrotto dopo:
using static Foo;
class Program
{
static void Main() => Bar("hello");
}
Ulteriori informazioni: https://github.com/dotnet/csharplang/issues/665