Streams i .NET

Tags:    .net
Skrevet af Bruger #4522 @ 20.04.2008

Klassen Stream


Den abstrakte klasse Stream er grundpillen i streams-arkitekturen. Det er fra den stream klasserne nedarver. Som nævnt tidligere er stream klasserne byte-orienterede så når vi brugder dem læser, skriver eller søger vi bytes (og som også nævnt bruger vi stream adaptere såfremt vi ønsker at læse/skrive tekst, ints, xml, osv.).

Da alle de stream klasser man bruger nedarver fra Stream-klassen er det vigtigt at forstå dens funktionalitet.

Når vi har oprettet en stream via dens konstruktør (constructor) er der tre "offentlige" (dvs. public) properties vi kan bruge til at sikre os at den understøtter det vi ønsker før vi udfører handlingen: CanRead, CanWrite og CanSeek.

CanRead bruges før vi læser fra en stream for at sikre os at den nuværende stream understøtter det. Der returneres sand såfremt der kan læses fra datastrømmen, ellers returneres der falsk. Hvis man læser fra en strøm der ikke understøtter en læsehandling, kastes en undtagelse af typen NotSupportedException.

Hvis du arbejder med en datastrøm hvis lager-steam er en FileStream eller en MemoryStream kan du søge (på eng.: seek) i strømmen, hvilket betyder at du kan specificere en position i strømmen hvorfra du ønsker at læse/skrive. Andre lager-streams, såsom en NetworkStream, understøtter ikke muligheden for at søge i datastrømmen. For at finde ud af om den datastrøm du arbejder med understøtter seeking, kan du bruge den property der hedder CanSeek. Der returneres sand hvis søgning understøttes, ellers falsk. Igen kastes en NotSupportedException såfremt der forsøges at udføre en seek-handling på en strøm der ikke understøtter dette. Hvis datastrømmen understøtter søgning, bruges den property der hedder Position til at sætte/oplyse den nuværende position i datastrømmen - dvs. den bruges til at navigere rundt i strømmen med.

Det er ganske sikkert at du nu kan regne ud hvad den property der hedder CanWrite gør, og hvordan den bruges. Men for en god ordens skyld: CanWrite bruges til at undersøge om en datastrøm understøtter muligheden for at skrive til den. Der returneres sand hvis det er tilfældet eller falsk. Hvis du er fræk og alligevel skriver til en datastrøm der ikke understøtter det, kastes en NotSupportedException.

Den property der hedder Length, og som der kun kan læses fra, returnerer en værdi af typen long som angiver datastrømmens længde i bytes. Den bruges f.eks. når man skal finde ud af hvor stor en array med bytes man skal bruge til at lagre strømmen i.

Når vi skal i gang med at arbejde med vores datastrøm har Stream, ikke overraskende, nogle metoder til det. Der er metoder der giver os mulighed for at læse fra strømmen, skrive til den, eller søge (altså seeke) i den.

Lad os starte med det sidst: seeke. Som nævnt tidligere kan du sætte din ønskede nuværende position (hvilket netop er at seeke) ved brug af den property der hedder Position. Når du bruger Poistion kalder den faktisk en metoder der hedder Seek(). Du kan også selv bruge Seek() direkte i stedet hvis du ønsker. Når du seeker laver du faktisk det der hedder en random access i datastrømmen - dvs. du specificer selv helt præcist hvorfra du ønsker at foretage en læse/skrive handling fra/på datastrømmen. Dette vil du gøre når du ønsker adgang til specifikke dele af den strøm du arbejder med. Seek() metoden tager to argumenter: en long som er den afstand i bytes du ønsker fra et referencepunkt i strømmen. Referencepunktet sættes med det andet argument som er af typen SeekOrigin. SeekOrigin er en enum-datatype, og har følgende medlemmer: Begin, Current og End.

Så hvis du f.eks. bruger Seek() sådanne:

Fold kodeboks ind/udKode 


har du sat en ny position i datastrømmen som er 100 bytes fra dens begyndelse. Og du kan altså herfra begynde at læse og/eller skrive.

Gør du derimod følgende:

Fold kodeboks ind/udKode 


sætter du din nye position i datastrømmen til at være 50 bytes fra den nuværende position.

De metoder vi bruger til at læse til og skrive fra en datastrøm er inddelt i to grupper: de synkrone og de asynkrone.

De synkrone metoder hedder Read(byte[] buffer, int offset, int count), ReadByte(), Write(byte[] buffer, int offset, int count) og WriteByte(byte value).

Read(byte[] buffer, int offset, int count) læser count antal bytes fra strømmen og gemmer dem i buffer fra position offset. Der kan være færre end count bytes i datastrømmen, så count er det maksimale antal læste bytes. Efter læsningen er din nye position i datstrømmen avanceret det læste antal bytes.

ReadByte() læser en enkelt byte, returnerer den og avancerer positionen i datastrømmen med en byte. Hvis der ikke er mere at læse fra datastrømmen, returneres i stedet værdien -1.

Write(byte[] buffer, int offset, int count) bruges til at skrive til datastrømmen. Der skrives count antal bytes fra position offset i buffer til datstrømmens nuværende position. Positionen i datastrømmen er derefter avanceret det skrevne antal bytes.

WriteByte(byte value) skrive byten value til datastrømmen, og avancerer positionen i datastrømmen én byte.

Det var de synkrone metoder. Nu til de asynkrone.

Vi starter med BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, Object state). Denne metoder starter en asynkron læsning fra datastrømmen. buffer er der hvor de læste bytes skal gemmes. offest er den position i buffer lagringen af de læste bytes skal begynde. count er det maksimale antal bytes der skal læses. callback, som er valgfri, er en delegate der skal kaldes når læsningen er fuldført. Det sidste argument er et eller andet objekt som kan bruges til at adskille denne asynkrone forespørgsel fra en anden. Den nuværende position i datastrømmen opdateres med det samme og ikke først når læsningen er udført. Når du kalder BeginRead returnerer metoden et objekt at typen IAsyncResult som er en reference til den asynkrone læseforespørgsel du lige har startet.

Asynkron skrivning til en strøm, initieres med BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, Object state). Denne metode skriver data fra position offest i tabellen buffer. Der skrives maksimalt count bytes til datastrømmen. Som ovenfor er callback en delegate som kaldes når skrivningen er fuldført - det er frivilligt om hvorvidt man ønsker at gøre brug af dette argument. state er ligeså som ovenfor - et objekt der bruges til at skelne denne asynkrone forespørgsel fra en anden. Den nuværende position i datastrømmen opdateres med det samme og ikke først når skrivningen er udført. Når du kalder BeginWrite returnerer metoden et objekt at typen IAsyncResult som er en reference til den asynkrone skriveforespørgsel du lige har startet.

Når du har startet en asynkron læse eller skrive forespørgsel er det vigtigt at du husker at afslutte den. Det gøres med metoderne EndRead og EndWrite respektivt, som blokerer indtil I/O-handlingen er udført. Disse metoder tager begge et argument af typen IAsyncResult som er en reference til den forespørgsel du ønsker at vente på bliver afsluttet. Der returneres en int som er antallet af bytes læst fra datastrømmen. For hver gang du kalder BeginWrite/BeginRead skal du altid kalde den tilsvarende EndWrite/EndRead. Hvis du ikke afslutter din asynkrone forespørgsel kan du opleve uønsket adfærd fra dit program, inklusiv for eksempel en såkaldt deadlock. Man vil typisk kalde EndRead/EndWrite fra den delegate man gav da man kaldte den tilsvarende BeginRead/BeginWrte.

Når du arbejder med en Stream-nedarvet klasse skal du også kende til Flush()-metoden. Den sørger for at al data der er i en buffer skrives til den pågældende underliggende enhed.

Du kan også kalde Close()-metoden. Close() sørger for at alle resurser der er tilknyttet strømmen, såsom filhåndtag, netværksforbindelser (socket) osv., bliver frigjort. Når du kalder Close() "flushes" der også automatisk så du behøver ikke selv at kalde Flush() før Close().

Med metoden SetLength() kan du sætte en datastrøms længde. Den tager et long argument som er den længde i bytes du ønsker strømmen skal være. Hvis den værdi du angiver er mindre end datastrømmens nuværende længde bliver datastrømmen forkortet. Derimod bliver datastrømmen forlænget hvis den længde du angiver er større end den nuværende længde; hvad der er indeholdt i den nye del af strømmen er udefineret. En datastrøm skal selvklart understøtte både seeking og skrivning før du kan bruge SetLength() (hvilket du kan undersøge med de to properties CanWrite og CanSeek som forklaret tidligere).

De properties og metoder vi netop har diskuteret er disse:



Nu da du ved hvad idéen er bag metoderne som de forskellige klasser der nedarver fra Stream implementerer - burde det være ligetil at bruge de forskellige lager-stream klasser. Vi vil nu kigge på hvordan FileStream kan bruges. FileStream nedarver fra Stream så vi vil bl.a. gøre brug af de properties og metoder vi netop har diskuteret.


Hvad synes du om denne artikel? Giv din mening til kende ved at stemme via pilene til venstre og/eller lægge en kommentar herunder.

Del også gerne artiklen med dine Facebook venner:  

Kommentarer (2)

User
Bruger #6559 @ 20.04.08 11:17
En meget god artikel. Jeg synes at når du begynder at skrive om MemoryStream, så er det synd du ikke går mere i dybden med det når du alligevel begynder at fortælle lidt om klassen. Der burde du måske bare have undværet det helt og så skrevet i starten at det ikke var med i artiklen. Ellers faldt jeg ikke over det helt store. Godt beskrivende og helt sikkert noget jeg kommer til at bruge. Keep up the good work :)
User
Bruger #4522 @ 20.04.08 15:54
Tak for dine kommentarer Martin. Du har en pointe omkring MemoryStream og jeg vil prøve at uddybe det lidt mere i artiklen.
Du skal være logget ind for at skrive en kommentar.
t