Nej til null: Her er nyhederne i C# 8

Illustration: Bigstock/REDPIXEL.PL
Den næste udgave af Microsofts sprog er ikke langt væk. Vi ser nærmere på nyhederne, som blandt andre er forbud mod null-værdier, intervaltyper og asynkrone streams.

En ny udgave af C#, Microsofts flagskib blandt .NET-sprogene, er snart på trapperne.

».NETCore 3.0 er tæt på færdiggørelse, og holdet bag fokuserer udelukkende på stabilitet og pålidelighed, nu hvor vi ikke længere bygger nye faciliteter,« skrev Richard Lander, som er Program Manager i .NET-teamet i et blogindlæg i sidste uge.

Det seneste preview af .NET Core 3, kom på gaden i sidste uge, har nummeret syv og og kommer med C# 8 i favnen.

Der er mange nyheder i den nye version, og sprogets chef-designer, Mads Torgersen, der har overtaget tøjlerne fra opfinder Anders Hejlsberg, har tidligere peget et par stykker af dem ud i et blogindlæg.

Mads Torgersen har en fortid som lektor og ph.d i datalogi fra Aarhus Universitet, så sproget bliver på danske hænder. Og det kan vi vel godt tillade os at være lidt stolte over.

Nej til null

Null-værdier er et smertensbarn i objektorienterede sprog, og sådan har det været i ganske mange år, mener Mads Torgersen:

»Vi har foregivet det de sidste 50 år med objektorienteret programmering, men faktisk er null ikke et objekt. Derfor eksploderer alt, når du prøver at behandle det som om det er et. Så ikke mere af det: Null er verboten, medmindre du beder om det. Hvordan beder du om det? Ved at bruge en ‘nullable’ referencetype, såsom streng? - det efterfølgende spørgsmålstegn signalerer, at null er tilladt her.«

I Visual Studio skal den nye facilitet slås til i projekt-filen .csproj, hvor linjen

<NullableReferenceTypes>true</NullableReferenceTypes>

– skal tilføjes. Og det skal også være preview-udgaven af Visual Studio, der anvendes.

Hvis man skriver følgende program, der i øvrigt indeholder en tikkende bombe - en null-initieret variabel:

 
using static System.Console;
 
class Program
{
    static void Main(string[] args)
    {
        string s = null;
        WriteLine($"The first letter of {s} is {s[0]}");
    }
}

– giver udviklingsværktøjet en fejl: Der klages over, at en ‘ikke-nullable type’ tildeles værdien null.

Hvis man alligevel vil tildele s værdien null, skal det gøres med det tidligere nævnte spørgsmålstegns-operator:

string? s = null;

Men den næste linje, hvor s udskrives, giver stadig bøvl. Compileren kan, se, at s eventuelt kan være null. Problemet løses på denne vis:

if (s != null) WriteLine($"The first letter of {s} is {s[0]}");

– hvor compileren kan regne ud, at s ikke kan være null inde i kroppen på if-sætningen.

En anden og mere kompakt måde at opnå det samme på, er:

 WriteLine($"The first letter of {s} is {s?[0] ?? '?'}");

– hvor spørgsmålstegnsoperatoren returner null eller en char, afhængigt af, om s er null eller ej, og ??-operatoren udskifter null med tegnet ‘?’.

Mads Torgersen følger faciliteten til dørs med denne kommentar:

»Som du kan se, holder faciliteten dig fast på redeligheden, mens du koder: Den tvinger dig til at udtrykke din hensigt, når du vil have null i systemet, ved at bruge en 'nullable' referencetype. Når først null er til stede, tvinger det dig til at håndtere det på en ansvarlig måde, hvilket får dig til at kontrollere, når der er risiko for, at en null-værdi muligvis kan blive referet til, for så at udløse exception.«

Intervaltyper

Ranges er en ny intervaltype, ligesom det kendes fra eksempelvis Pascal. Det kan benyttes til at definere delintervaller, som i eksemplet herunder. Det er en feature, som kan minde om ‘slices‘ i sprog som Python og Rust.

 
using System.Collections.Generic;
using static System.Console;
 
class Program
{
    static void Main(string[] args)
    {
        foreach (var name in names[1..4])
        {
            WriteLine(name);
        }
    }
 
    static IEnumerable<string> GetNames()
    {
        string[] names =
        {
            "Archimedes", "Pythagoras", "Euclid", "Socrates", "Plato"
        };
        foreach (var name in names)
        {
            yield return name;
        }
    }
}

En Range kan skabes således

Range range = 1..4; 

– hvor 1 er inklusiv og 4 eksklusiv, og endepunkterne er af typen Index.

En ny operator ^ er det også blevet til, og den betyder ‘fra slutningen af.’ Så intervallet

foreach (var name in names[1..^1])

– betyder fra og med indeks 1 til én fra slutningen. Her i eksemplet returneres derfor de tre elementer i midten af arrayet.

Ranges kan med preview 7 bruges med arrays, string.SubString, Span.Slice og AsSpan-extension-metoder.

Asynkrone streams

Den sidste af nyhederne, som Mads Torgersen har valgt at fremhæve, er asynkrone streams, som i eksemplet herunder:

 
using System.Collections.Generic;
using static System.Console;
using System.Threading.Tasks;
 
class Program
{
     static async Task Main(string[] args)
    {
        await foreach (var name in GetNamesAsync())
        {
            WriteLine(name);
        }
    }
 
    static async IAsyncEnumerable<string> GetNamesAsync()
    {
        string[] names =
        {
            "Archimedes", "Pythagoras", "Euclid", "Socrates", "Plato"
        };
        foreach (var name in names)
        {
            await Task.Delay(1000);
            yield return name;
        }
    }
}

Her simuleres en asynkron arbejdsbyrde med en asynkron pause i linjen await Task.Delay(1000).

Interfacet IAsyncEnumerable er er asynkron udgave af IEnumerable.

Den resulterende metode GetNamesAsync() await’er så hvert enkelt element i stream’en.

.NET Core 3.0 preview 7 kan downloades fra Microsofts hjemmeside.

Tips og korrekturforslag til denne historie sendes til tip@version2.dk
Følg forløbet
Kommentarer (16)
sortSortér kommentarer
  • Ældste først
  • Nyeste først
  • Bedste først
Magnus Jørgensen

Jeg er ikke helt sikker på at jeg forstår formålet med dette.
Er det for at optimere koden således at der ikke behøves at checkes for NullReferenceException ved alle kald til objecter?
Det kunne give mening.
Eller er der en årsag der bunder i sikkerhed der gør NullReferenceException til et problem?

Bjarne Nielsen

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-M...

Jonas Høgh

Nej, der er ikke noget "usikkert" ved NullReferenceException i .net, den bliver bare sendt op ad stakken til nærmeste relevante catch-block, eller får miljøet til at lukke pænt ned hvis den ikke bliver håndteret, ligesom enhver anden exception.

Den nye feature handler kun om at udvide typesystemet, så reference-typer ikke behøver at være nullable. Det handler nok mere om at gøre det lettere at skrive korrekt kode, end at null-checks er dårlige for performance.

Kasper Hansen

Som allerede skrevet. Det handler om at forhindre bugs (ved at gøre det nemmere at skrive korrekt kode).

Jeg læste engang en undersøgelse af hvilke typer bugs der var mest normale i et stort kommercielt C# projekt og noget i retning af 80% af alle bugs var uhåndterede Null Exceptions.

Null handling er svært. og selv Null checks er svære fordi du risikerer bare at skubbe fejlen til et andet sted i koden der ikke tjekker korrekt.

Dette er nok den feature i C# jeg har set mest frem til i årevis. Desværre bliver den meget svær at implementere i eksisterende prokekter da den er fuldstændig breaking så en større kodebase kan se frem til et stort arbejde for at implementere det... Men det er stadig en fantastisk nyhed.

Bjarne Nielsen

Den nye feature handler kun om at udvide typesystemet, så reference-typer ikke behøver at være nullable.

På mig virker det, som en naturlig ting at have, hvis man har et typesystem.

Vi har tal som type, fordi at så kan vi uden videre gøre talting med dem. Vi har strenge som type. fordi at så kan vi uden videre gøre strengting med dem. Og vi bliver advaret, hvis vi prøver at gøre talting med strenge og omvendt.

Vi kunne også have et sprog, hvor alt kunne være tal eller strenge, og så ville vi skulle håndtere (mentalt eller i kode) at det måske er et tal eller måske en streng.

Men man har så valgt, at have taltyper til tal og strengtyper til strenge. Og hvad gør man så? Jo, man lave det til måske-tal og måske-strenge. Så nu skal vi til (mentalt eller i kode) at håndtere at det måske er et tal og måske ikke. Eller måske en streng og måske ikke. Og så har vi ikke fuld nytte af at lave et typesystem.

For mig at se, at det ganske naturligt, at det er nyttigt at have tal og streng, og måske-tal og måske-streng, som typer.

Bjarne Nielsen

Om det i praksis er for sent at indføre det i C# vil jeg vente med at gisne om, til jeg har prøvet C# 8 på et rigtigt projekt.

Iom. at det er endnu en finurlighed i et sprog, som allerede har mange finurligheder, så ville jeg nok have fortrukket, at det var noget, som var sket i et nyt sprog. Ideen er fin, men bagudkompatibilitetshensyn ender nok i praksis med at øge kompleksiteten, hvor det i virkeligheden helst skulle reducere den.

På den anden side, så skal der nok være nogle få derude, som vil få stor nyttte af det, og det er meget nemmere at få en beslutning om at sætte en kompiler-option igennem, end det vil være at få en beslutning om at skifte til et nyt sprog (også selvom kompiler-optionen reelt gør det til et nyt sprog). Og ud fra den betragtning, så er det aldrig for sent ... :-).

Torben Mogensen Blogger

Microsoft lavede for mange år siden en eksperimentel udgave af C#, som de kaldte Spec#. Ideen var at man kunne skrive specifikationer ind i koden, som så kunne verificeres. Spec# havde som default ikke null-referencer, så de skulle erklæres eksplicit ligesom i C# 8. Det har bare taget lang tid, før denne facilitet er kommet til C#.

Se endvidere https://www.version2.dk/blog/ned-med-null-7472

Martin Bæk

Jeg tror, at man i praksis ender med at lave så godt som alle reference-type variabler nullable. Men så vi vi jo lige vidt.
Jeg synes faktisk ikke, at ændringen er en god ide. Tværtimod synes jeg, at det er helt naturligt, at have null-værdier. Det betyder jo bare, at der ikke er en værdi. "At data mangler". Det svarer til virkelighed.
Det kommer blot til at tage en masse tid at opgradere til C#8.

Sune Marcher

Jeg tror, at man i praksis ender med at lave så godt som alle reference-type variabler nullable. Men så vi vi jo lige vidt.


Lige vidt? Nej, ikke når compileren kræver at du laver null-check før du får lov at referere en nullable type.

Jeg synes faktisk ikke, at ændringen er en god ide. Tværtimod synes jeg, at det er helt naturligt, at have null-værdier. Det betyder jo bare, at der ikke er en værdi. "At data mangler". Det svarer til virkelighed.


Det ville være bedre at afskaffe null til fordel for en optional type, men det er ikke realistisk i et eksisterende sprog - denne ændring (der lyder ret meget som Kotlins løsning?) er en fin pragmatisk løsning.

Hans Nielsen

Pass.
Det er godt nok langt tiden siden jeg har programeret.

Og denne tråd, får mig bestemt ikke min lyst til at komme igen ;-)

Men skal nok snart igang med noget C og ligende igen, da jeg vil se på noget andriono, og smart styring til hjem.

Men lysten,,, det er så meget at at sp.. bruge tiden på.

Men dejligt at se, at min beslutning om ikke at gå denne vej, var helt rigtigt. Kan se, der findes mange, som har meget mere styr på programering, end jeg nogensinde ville have opnået.

PS. Ellers så var det mig som bare var lidt uheldigt i tiden, og med tiden. Jeg kunne programere til husbehov i C i et komando milijø. Også kom windows, og ikke altid lige gode værktøj til at lave GUI. Sammen med studie hvor tiden var sparsom, så gik det helt i stå.

Men har startet lidt med Andriono, og det var faktisk ganske morsomt, lige hvor man sluttet i 80'erne. Samt med meget (som rigtigt meget) bedre værktøjer til at hjælpe en med syntax og programering. Det skal jeg helt sikkert igang med når der kommer mørke vinter aftener.

Beklager dette lidt off trop. indlæg. Men blev helt nostalgisk da jeg læste tråden.

Michael Fjeldsted

Jeg synes faktisk ikke, at ændringen er en god ide. Tværtimod synes jeg, at det er helt naturligt, at have null-værdier. Det betyder jo bare, at der ikke er en værdi. "At data mangler". Det svarer til virkelighed.

Og det kan du også nemt gøre, du skal bare angive at du forventer null værdier - så fortæller compileren dig hvis din kode ikke tager højde for nullværdier.
Alternativt behøver du ikke tage højde for null, hvis aldrig den situation opstår.
Det nye er du nu kan fortælle om du forventer null værdier eller ej. Om compileren så hjælpe dig hvis du forventer null, men har glemt null checks et sted i koden. På den måde kan compileren hjælpe dig til ikke længere at have null reference exceptions - med eller uden null værdier.

Det kommer blot til at tage en masse tid at opgradere til C#8.


Nej - det er funktionalitet som kan slåes til og fra. Hvis ikke du slår det til, så er der ingen bracking changes. Og du kan så bruge noget af alt det andet nye i C# 8. I et nyt projekt vil det være noget man kan tage med fra start, men ikke noget der er tvunget.

Baldur Norddahl

Jeg tror, at man i praksis ender med at lave så godt som alle reference-type variabler nullable. Men så vi vi jo lige vidt.
Jeg synes faktisk ikke, at ændringen er en god ide. Tværtimod synes jeg, at det er helt naturligt, at have null-værdier. Det betyder jo bare, at der ikke er en værdi. "At data mangler". Det svarer til virkelighed.

Der er mange sprog der ikke har null. Alligevel ser det ud til at folk kan kode i dem :-).

Nu kender jeg ikke så meget til c# så jeg vil tage et eksempel fra Java og Scala i stedet. Hvis du laver et opslag i et Java hashmap med en ukendt nøgle, så returneres der null. Som du siger, data findes ikke. Det kan så potentielt være årsag til en uventet null pointer exception hvis programmøren glemmer at tjekke for null.

I Scala returnerer samme kald et såkaldt Option objekt i stedet. Option har to nedarvninger, Some(data) og None. Hvis der ikke er noget data at returnere, så returneres None. Aldrig null. For at få fat i data skal programmøren tage stilling til begge cases. Eksempelvis ved at kalde metoden getOrElse(default_value). Eller kalde map som ingen effekt har på None.

Null betyder egentlig uinitialiseret og misbruges blot i mange sammenhænge til andet.

Brian Vraamark

@Brian Mon ikke at du så får deserialization exception, det vil da være det mest logiske

Nej, du får jo stadigvæk en kompiler warning når du ikke laver en initialisering i en constructor (i min verden er warning==error). Det du snakker om er jo runtime.

Jeg synes at de skulle implementere noget i stil med Kotlin's "lateinit".

Rust's løsning er endnu bedre. Den giver kompiler fejl (ikke warnings) hvis den ikke er initialiseret inden du læser den. Med andre ord, så SKAL den første operation altid være en skrivning.

Log ind eller Opret konto for at kommentere