FileUtility PathInfo

FileUtility är ett bibliotek för Windows/.NET 6.0 som (för närvarande) innehåller en enda klass: PathInfo. Den kan bl.a. korta ner en filsökväg för presentation på skärmen och ange fil- och katalogstorlek i flera format, inklusive ett människovänligt textformat. Exempelkod finns här.

var max = 15;
var f = new PathInfo(new FileInfo(@"C:\Temp\hello.txt"));
Console.WriteLine(f.CompactPathForDisplay(15))

// Result: C:...\hello.txt

Sprite (OpenGL i .NET Framework)

Jag har dragit nytta av ett gammalt C#-repository som wrappar OpenGL för .NET Framework för att bygga ett minimalistiskt spelramverk. Följande kod visar stjärnor som flyger över skärmen, från höger till vänster. Projektet är .NET Framework 4.8 som jag valde för att det finns inbyggt Windows 10/11.

Exemplet visar initiering av spelmotorn, att skapa sprites med GDI+, användning av en scen, tangentbordsavläsning, en sprite batch (som erbjuder fire-and-forget sprites).

Projekttypen är en console application, och referenserna är Sprite och System.Drawing. Repositoryt finns här och detta är den kompletta koden:

using System;
using System.Drawing;
using Sprite;

namespace Starfield
{
    public class Program
    {
        public static SpriteBitmap Star;
        public static GameEngine GameEngine { get; private set; }

        public static void Main()
        {
            // Create and initialize the game engine.
            GameEngine = new GameEngine("Starfield", OnLoadResources);
            GameEngine.LoadResources();

            // Run the game.
            GameEngine.Run(new StarfieldScene());
        }

        private static void OnLoadResources(object sender, EventArgs e)
        {
            // Draw a "star".
            var star = new Bitmap(2, 2);
            star.SetPixel(0, 1, Color.White);
            star.SetPixel(0, 0, Color.White);
            star.SetPixel(1, 1, Color.White);
            star.SetPixel(1, 0, Color.White);

            // Create a sprite bitmap from the star.
            Star = SpriteBitmap.FromImage(star, 1, 1);

            // Tell engine that loading is completed.
            GameEngine.SetLoadResourcesComplete();
        }
    }

    // Define the star sprite that will fly from right to left.
    public class StarSprite : BatchGameSprite
    {
        private int SpeedX { get; }

        public StarSprite(SpriteBitmap spriteBitmap, int y, int speedX) : base(spriteBitmap, 320, y, 1)
        {
            SpeedX = speedX;
        }

        public override void ApplyLogic() =>
            X += SpeedX;

        public override bool Alive() =>
            X > -5;
    }

    public class StarfieldScene : IScene
    {
        private readonly Random _rnd = new Random();

        // Create a sprite batch (used for fire-and-forget sprites) to hold the stars.
        private readonly SpriteBatch _spriteBatch = new SpriteBatch();

        public void Render(GameEngine gameEngine)
        {
            // Check if the user wants to exit.
            if (gameEngine.SpriteWindow.IsKeyDown(VirtualKeys.Escape))
                gameEngine.SpriteWindow.Running = false;

            // Add a new star each frame.
            _spriteBatch.Add(new StarSprite(Program.Star, _rnd.Next(199), -(_rnd.Next(5) + 1)));

            // Make the stars act.
            _spriteBatch.ApplyLogic();

            // Draw the frame.
            _spriteBatch.Draw(gameEngine.SpriteWindow);
            gameEngine.SpriteWindow.Swap();
        }
    }
}

Mutable strings i C#

Den strängtyp som finns inbyggd i C# är “immutable”, vilket betyder att den inte kan ändra värde. Jag har skapat en “mutable string”, som kapslar strängtypen i en klass. Det innebär att man kan anropa funktioner på strängen för att få den att ändra värde. Dessa funktioner, och funktionerna för att analysera strängens innehåll, har färdiga implementationer, men dessa kan ersättas. T.ex. kan funktionen Is svara på frågan om två strängar har samma värde, enligt de flaggor som angavs när strängen skapades, eller så kan man skicka med egen kod som utför jämförelsen som argument till funktionen Is. Mer information finns här: https://github.com/Anders-H/MutableStringLibrary

Input parser för textäventyrsspel

Biblioteket TextAdventureGameInputParser kan användas för att parsa användarens input i ett textäventyr. Parsern tar en mening i stil med GO NORT, OPEN DOOR, USE GOLD KEY ON DOOR eller GIVE FOOD TO GANDALF och ger en struktur med ordklasser och referenser till objekt som är registrerade i parsern.

Lite exempel finns tillgängliga i parserns GitHub-repository, och parsern kan installeras (.NET 5.0) från Nuget.

Lorenz vattenhjul

Lorenz-attraktionen är en fraktal vars formel beskriver en rotationshastighet (illustrerat med en radie) och en rotationsriktning. Så här ser den ut, implementerad i Commodore BASIC 7.0:

10 GRAPHIC 1,1
20 X=5
30 Y=5
40 Z=5
50 T=0
60 S=1/200
70 D=10
80 R=28
90 B=8/3
100 T=T+0.1
110 DX=D*(Y-X)
120 X1=X+DX*S
130 DY=(R*X-Y)-X*Z
140 Y1=Y+DY*S
150 DZ=X*Y-B*Z
160 Z1=Z+DZ*S
170 X=X1
180 Y=Y1
190 Z=Z1
200 DRAW 1,150+4*X,20+3*Z
210 IF T<1000 GOTO 100

Det går att bygga ett riktigt vattenhjul som ger samma figur som algoritmen ovan. Tänk dig ett hjul med ett antal hinkar (t.ex. åtta stycken). Den hink som är högst upp fylls på med vatten, och att du ger hjulet en knuff så att det roterar åt höger. På det viset kommer nästa hink strax börja fyllas med vatten, vilket ger en vikt på hjulets högra sida, så att rotationshastigheten ökar. Men alla hinkar har ett hål i botten, så när de inte ökar i vikt för att de fylls med vatten, så minskar de i vikt. När hinkarna når den uppåtgående vänstersidan av hjulet, väger de mycket mindre, vilket bidrar till att rotationshastigheten ökar. Men om rotationshastigheten ökar, så minskar samtidigt mängden vatten som fylls på i hinken högst upp, eftersom hinken befinner sig kortare tid vid positionen för påfyllning. Det innebär att den tyngsta sidan inte alltid kommer vara högersidan, eftersom den höga rotationshastigheten och den låga påfyllningen får vikten att förflytta sig. Ibland roterar alltså hjulet åt höger, ibland åt vänster. Ibland roterar hjulet fort, ibland långsamt.

Centralt i implementationen av denna simulering är en funktion som kan omvandla en vinkel på hjulet till en rotationskraft. Högst upp eller längst ner på hjulet, kommer vikten inte att påverka hjulets vilja att rotera alls. Längst till höger eller längst till vänster är kraften som störst. Vid 0 grader och vid 180 grader ska kraften vara 0, vid 90 och 270 grader är kraften maximal. Följande funktion är central för implementationen. Den beskriver hur starkt grepp gravitationen har över hinken – ingen alls högst upp eller längst ner, väldigt mycket längst till höger eller vänster.

Math.Cos(angle / (180.0 / Math.PI));

Implementationen av hjulet har 8 entiteter (“hinkar”) utplacerade med jämna mellanrum. 8 genom 350 ger 45 graders mellanrum. Den lilla knuffen åt höger får ha värdet 0,2 så att rotationen kommer igång.

var angle = 0;
for (var i = 0; i < 8; i++)
{
    Wheel.Buckets.Add(new Bucket(Wheel, angle));
    angle += 45;
}
Wheel.Speed = 0.2;

För hjulets beteende gäller följande: Hastigheten (som är positiv för höger och negativ för vänster) adderas till vinkeln. Därefter kontrollerar vi att vinkeln ligger mellan 0 och 360. Därefter berättar vi för varje hjul vilken vinkel de sitter på, givet hjulets vinkel, och ber samtidigt hjulet att agera (nedan). Sedan räknar vi ut hur hinkkonfigurationen påverkar hastigheten, och kontrollerar samtidigt att maxhastigheten åt höger eller vänster inte överstiger hjulets tänkta maxhastighet.

public void Tick()
{
    Angle += Speed;
    if (Angle < 0.0)
        Angle += 360.0;
    else if (Angle > 360.0)
        Angle -= 360.0;

    var bucketAngle = Angle;

    for (var i = 0; i < 8; i++)
    {
        if (bucketAngle < 0.0)
            bucketAngle += 360.0;
        else if (bucketAngle > 360.0)
            bucketAngle -= 360.0;
            
        Buckets[i].Tick(bucketAngle);
        bucketAngle += 45.0;
    }
    Speed += SpeedInfluence();
    const double maxSpeed = 7.0;
    if (Speed > maxSpeed)
        Speed = maxSpeed;
    else if (Speed < -maxSpeed)
        Speed = -maxSpeed;
}

public double SpeedInfluence() =>
    Buckets.Sum(x => x.SpeedInfluence());

För varje hinks beteende gäller följande: Om hinken är högst upp, öka vattenmängden, annars minska vattenmängden en aning. Varje hjul måste kunna uppge gravitationens grepp om hinken.

public void Tick(double newAngle)
{
    Angle = newAngle;
        
    if (Angle >= 255.0 && Angle <= 285.0)
        Full += 2.0;

    Full -= 0.12;
        
    if (Full < 0.0)
        Full = 0.0;
    else if (Full > 100.0)
        Full = 100.0;
}

public double SpeedInfluence() =>
    Math.Cos(Angle / (180.0 / Math.PI)) * (Full * 0.03);

Endast vattnet har vikt, hinkarna väger ingenting. Eftersom hjulet är balanserat och alla tomma hinkar väger lika mycket, kan vikten lika gärna vara 0.

Resultatet blir ett hjul som ibland snurrar fort, ibland långsamt, ibland åt höger och ibland åt vänster.

Koden körs i ett Windows Forms-fönster med DoubleBuffering aktiverat. Här är hela källkoden:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;

public partial class Form1 : Form
{
    private Wheel Wheel { get; }

    public Form1()
    {
        InitializeComponent();
        Wheel = new Wheel();
        InitializeWheel();
    }

    private void InitializeWheel()
    {
        var angle = 0;
        for (var i = 0; i < 8; i++)
        {
            Wheel.Buckets.Add(new Bucket(angle));
            angle += 45;
        }
        Wheel.Speed = 0.2;
    }

    private void Form1_Paint(object sender, PaintEventArgs e)
    {
        var g = e.Graphics;
        g.Clear(Color.Green);

        var centerX = (Bounds.Width / 2);
        var centerY = (Bounds.Height / 2);
            
        var availableSize = Bounds.Width > Bounds.Height
            ? Bounds.Height
            : Bounds.Width;

        var radius = availableSize * 0.3;

        var renderer = new Renderer(centerX, centerY, (float)radius);
        renderer.Draw(g, Wheel, Font);
    }

    private void timer1_Tick(object sender, EventArgs e)
    {
        Wheel.Tick();
        Invalidate();
    }
}

public class Wheel
{
    public List<Bucket> Buckets { get; set; }
    public double Speed { get; set; }
    public double Angle { get; set; }

    public Wheel()
    {
        Buckets = new List<Bucket>();
        Angle = 0.0;
    }

    public void Tick()
    {
        Angle += Speed;
        if (Angle < 0.0)
            Angle += 360.0;
        else if (Angle > 360.0)
            Angle -= 360.0;

        var bucketAngle = Angle;

        for (var i = 0; i < 8; i++)
        {
            if (bucketAngle < 0.0)
                bucketAngle += 360.0;
            else if (bucketAngle > 360.0)
                bucketAngle -= 360.0;
            
            Buckets[i].Tick(bucketAngle);
            bucketAngle += 45.0;
        }
        Speed += SpeedInfluence();
        const double maxSpeed = 7.0;
        if (Speed > maxSpeed)
            Speed = maxSpeed;
        else if (Speed < -maxSpeed)
            Speed = -maxSpeed;
    }

    public double SpeedInfluence() =>
        Buckets.Sum(x => x.SpeedInfluence());
}

public class Bucket
{
    public double Weight { get; set; }
    public double Angle { get; set; }
    public double Full { get; set; }

    public Bucket(double angle)
    {
        Weight = 0.0;
        Angle = angle;
        Full = 0.0;
    }

    public void Tick(double newAngle)
    {
        Angle = newAngle;
        
        if (Angle >= 255.0 && Angle <= 285.0)
            Full += 2.0;

        Full -= 0.12;
        
        if (Full < 0.0)
            Full = 0.0;
        else if (Full > 100.0)
            Full = 100.0;
    }

    public double SpeedInfluence() =>
        Math.Cos(Angle / (180.0 / Math.PI)) * (Full * 0.03);

    public void Draw(Graphics g, PointF location, Font font)
    {
        using var bucketBrush = new SolidBrush(Color.FromArgb(255, 255, 255));
        using var waterBrush = new SolidBrush(Color.FromArgb(0, 128, 255));
        var bucketRectangle = new RectangleF(location.X - 51, location.Y - 51, 102, 102);
        g.FillRectangle(bucketBrush, bucketRectangle);
        if (Full > 0.0)
        {
            var waterRectangle = new RectangleF(location.X - 50, (float)(location.Y - 50 + (100.0 - Full)), 100, (float)Full);
            g.FillRectangle(waterBrush, waterRectangle);
        }
        g.DrawString(Angle.ToString("0.00"), font, Brushes.Black, (float)(location.X - 20), (float)(location.Y - 20));
        g.DrawString(Full.ToString("0.00"), font, Brushes.Black, (float)(location.X - 20), (float)(location.Y));
    }
}

public class Renderer
{
    private readonly PointF _center;
    private readonly float _radius;

    public Renderer(float centerX, float centerY, float radius)
    {
        _center = new PointF(centerX, centerY);
        _radius = radius;
    }

    public void Draw(Graphics g, Wheel wheel, Font font)
    {
        foreach (var bucket in wheel.Buckets)
        {
            var bucketLocation = new PointF(
                (float)(_center.X + Math.Cos(bucket.Angle / (180.0 / Math.PI)) * _radius),
                (float)(_center.Y + Math.Sin(bucket.Angle / (180.0 / Math.PI)) * _radius)
            );
            g.DrawLine(Pens.Black, _center, bucketLocation);
            bucket.Draw(g, bucketLocation, font);
        }
    }
}

Det går garanterat att justera acceleration, gravitation, inflödeshastighet, utflödeshastighet och maximal hastighet för andra (bättre?) resultat.

GetEnumerator extension method

En ganska enkel men ytterst trevligt tillägg i C# version 9 är möjligheten att skapa en extension method av GetEnumerator (som konsulteras när en samling ska enumereras). Betrakta denna lilla lista:

var people = new List<string>
{
    "Sven",
    "Nils",
    "Niklas",
    "Janne"
};

Om jag skulle vilja ha samtliga poster i listan för utskrift på skärmen, kan denna kod användas:

foreach (var p in people)
    Console.WriteLine(p);

GetEnumerator används implicit. Men om jag bara är intresserad av att ha med poster vars förnamn börjar på N, så måste det formuleras någonstans. Jag kan t.ex. kopiera listan och exkludera ointressanta poster, eller så skulle jag kunna iterera genom svaret från LINQ-API:et enligt:

foreach (var p in people.Where(x => x.StartsWith("N")))
    Console.WriteLine(p);

Extension methods har fördelen att de är tillgängliga i filer som läst in dess namnrymd, men är i övrigt otillgängliga. Det innebär att man kan begränsa funktionalitet till vissa filer genom att skapa en extension metod. Om jag generellt sett är intresserad av att ha personerna som börjar på N tillgängliga i den aktuella filen (eller på annat sätt kunna använda manipulerat data) kan jag deklarera en egen GetEnumerator som en extension method. Är den tillgänglig, kommer kompilatorn att välja den. I detta fall kommer man troligtvis att vilja ha full tillgänglighet för metoden.

static class MyEnumerator
{
    public static IEnumerator<T> GetEnumerator<T>(this IEnumerator<T> list) =>
        list;
}

Med denna på plats, öppnas en ny möjlighet. Svaret från den funktionen, kan itereras. Nu kan jag skapa en enumerator som t.ex. skickar tillbaka personerna som börjar på N. Med det gjort, blir iterationen lika enkel som i första exemplet, fast med den skillnaden att jag nu kontrollerar dess innehåll.


var somePeople = people
   .Where(x => x.StartsWith("N"))
   .GetEnumerator();

foreach (var p in somePeople)
    Console.WriteLine(p);

Detta är egentligen inte en stor sak, men det bidrar till kodens elegans – något som har blivit ett av kännetecknen för C#.

Objektnotation: XML, JSON, PSON

Här följer en kort kommentar om tre olika format för textbaserad objektnotation.

XML

Fördelar: Stöd för dokumenttypsdefinitioner (DTD) och scheman. God tillgång på bra API:er.
Nackdelar: Mycket overhead (ett “pratigt” språk), endast Visual Basic har inbyggt stöd för formatet.

Exempel på notation:

<?xml version="1.0" encoding="utf-8" ?>
<customer>
	<id>551</id>
	<account>9153</account>
	<name>
		<firstName>Sven</firstName>
		<lastName>Hedin</lastName>
	</name>
</customer>

Exempel på inläsning (C#, otypat):

var dom = new XmlDocument();
dom.Load("object.xml");
var document = dom.DocumentElement;
var customerId = document!.SelectSingleNode("id")!.InnerText;
Console.WriteLine($"ID={customerId}");

JSON

Fördelar: Liten overhead, god tillgång på bra API:er. Minimal schematisk information (vad som är en array eller ett enskilt objekt) kan anges i formatet.
Nackdelar: Nästan ingen typsäkerhet.

Exempel på notation:

{
  "id": 551,
  "account": 9153,
  "name": {
    "firstName": "Sven",
    "lastName":  "Hedin" 
  } 
}

Exempel på inläsning (C#, otypat):

using var sr = new StreamReader("object.json", Encoding.UTF8);
var json = sr.ReadToEnd();
dynamic customer = JObject.Parse(json);
Console.WriteLine($"ID={customer!.id}");

Exempel på inläsning (C#, typat):

using var sr = new StreamReader("object.json", Encoding.UTF8);
var json = sr.ReadToEnd();
var customer = JsonSerializer.Deserialize<Customer>(json);
Console.WriteLine($"ID={customer!.Id}");

class Customer
{
    [JsonPropertyName("id")]
    public int Id { get; set; }
    [JsonPropertyName("account")] 
    public int Account { get; set; }
    [JsonPropertyName("name")] 
    public Name Name { get; set; }
}

class Name
{
    [JsonPropertyName("firstName")]
    public string FirstName { get; set; }
    [JsonPropertyName("lastName")]
    public string LastName { get; set; }
}

PSON

Fördelar: Liten overhead. Viss schematisk information kan anges i formatet, som vad som är en array eller ett enskilt objekt och objekttyp. Kan skrivas typsäkert.
Nackdelar: Kan endast användas från PowerShell.

Exempel på notation:

@{
  Id: 551,
  Account: 9153,
  Name: @{
    FirstName: "Sven"
    FastName:  "Hedin" 
  } 
}

Inläsning av den filen sker genom PowerShell och kan plockas upp av C# genom det generella gränssnittet Microsoft tillhandahåller för att nå svaret från PowerShell.

Datastorleken för XML är 188 bytes, JSON 110 bytes och PSON 104 bytes.

C# 9 Records

Hur projektmallarna pytsas ut i Visual Studio har verkligen övergått mitt förstånd. Beträffande .NET 5.0 var Windows Forms-mallen tillgänglig långt innan Console Application-mallen, men nu är äntligen allt på plats. Med anledning av det vill jag summera nyheterna i C# version 9 som, för att fungera fullt ut, kräver .NET 5.0. Här är den första:

Records
Records är klasser där man gjort avkall på vissa möjligheter för att komma åt möjligheten skapa objekt med properties på så lite kod som möjligt. Nyckelordet record anger att de medlemmar som följer ska vara publika (vilket inte stämmer enligt nuvarande officiell version) och om de inte ska kunna modifieras i programkoden, kan de nu deklareras som init (vilket faktiskt fungerar), vilket betyder public fram till första tilldelningen, därefter private.

using System;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var a = new Something {X = 10, Y = 20};
            Console.WriteLine(a.X);
        }
    }

    public record Something
    {
        public int X { get; init; }
        public int Y { get; init; }
    }
}

Skulle programmet fortsätta med tilldelningen a.X = 30 skulle kompilatorn påpeka att vår enda chans att lagra et värde i a.X är förbrukad.

Hur svårt är det att gissa ett tal?

En person tänker på ett tal mellan 1 och 100, och en annan ska gissa vilket. Om den som gissar skulle gissa fel, så får han reda på om han har gissat på ett för stort eller för litet tal. Den bästa strategin för att gissa rätt på så få försök som möjligt, är att försöka diskvalificera så många tal som möjligt.

Genom att gissa på 10 och få höra att talet är för stort, är genast 91 tal diskvalificerade, och rätt svar måste vara mellan 1 och 9, men man kan lika gärna (och troligen) få höra att talet är för litet, och då är bara 10 tal (1 till 10) diskvalificerade medan 90 tal fortfarande kan vara aktuella.

Smartast är att gissa på 50. Är det fel svar, diskvalificerar det ändå fler än hälften av alla tal mellan 1 och 100, oberoende av om 50 är för stort eller inte. Skulle det vara för stort gissar man på 25, skulle det vara för litet gissar man på 75, och så vidare. Den vinnande strategin för den som gissar på ett tal är alltså att dra av hälften av talrymdens storlek och addera det till eller dra bort det ifrån senaste gissning.

Eftersom det finns en bästa strategi för den som gissar, så kommer antalet försök att hitta ett tal alltid bli detsamma för ett specifikt tal. Personen tänker på talet 50, kommer den som gissar att hitta talet på första försöket. För att mäta hur lång tid det tar att hitta ett specifikt tal, och för att slippa sitta med en vän och gissa på tal hela veckan, har jag skapat två enkla robotar.

Roboten Conny kan tänka på ett tal och be någon att gissa. Roboten Steven kan gissa på tal, och är dessutom bekant med gissningsstrategin jag beskrev ovan. Detta är Conny:

public class NumberThinkingRobot
{
    private readonly ICanGuess _guesser;
    private readonly int _correct;

    public NumberThinkingRobot(int correct, ICanGuess guesser)
    {
        _correct = correct;
        _guesser = guesser;
    }

    public void Begin()
    {
        var tryCount = 0;
        do
        {
            Console.WriteLine($"Try no. {++tryCount}");

            var guess = _guesser.Guess();

            if (guess < _correct)
                _guesser.TooSmall();
            else if (guess > _correct)
                _guesser.TooLarge();
            else
            {
                Console.WriteLine("Correct!");
                break;
            }
        } while (true);
    }
}

Och detta är Steven:

public class NumberGuessingRobot : ICanGuess
{
    private readonly int _max;
    private int _correct;
    private double _stepSize;

    public NumberGuessingRobot(int max)
    {
        _max = max;
        _correct = _max/2;
        _stepSize = _correct;
    }

    public int Guess()
    {
        Console.WriteLine($"Guess: {_correct}");
        return _correct;
    }

    public void TooSmall()
    {
        Console.WriteLine("Too small!");

        _stepSize /= 2.0;
        _stepSize = _stepSize < 1
            ? 1
            : _stepSize;

        _correct += (int)Math.Round(_stepSize);
        _correct = _correct > _max
            ? _max
            : _correct;
    }

    public void TooLarge()
    {
        Console.WriteLine("Too large!");

        _stepSize /= 2.0;
        _stepSize = _stepSize < 1
            ? 1
            : _stepSize;

        _correct -= (int)Math.Round(_stepSize);
        _correct = _correct < 1
            ? 1
            : _correct;
    }
}

Programmet presenteras i sin helhet sist. Så låt oss instruera Conny att det korrekta talet är 22, och instruera Steven att det högsta tillåtna talet är 100, för att sedan fösa ihop dem.

using System;

const int max = 100;
const int correct = 22;
var steven = new NumberGuessingRobot(max);
var conny = new NumberThinkingRobot(correct, steven);
conny.Begin();

Vi kan konstatera att Steven behöver ha fem försök för att hitta 22. Detta är resultatet av körningen:

Try no. 1
Guess: 50
Too large!
Try no. 2
Guess: 25
Too large!
Try no. 3
Guess: 13
Too small!
Try no. 4
Guess: 19
Too small!
Try no. 5
Guess: 22
Correct!

Så hur svåra är de olika talen mellan 1 och 100 att hitta? 50 hittas direkt och 25 och 75 hittas på två gissningar. 13, 37, 63 och 87 kräver tre gissningar. 7 19, 31, 43, 57, 69, 81 och 93 kräver fyra. För 4, 10, 16, 22, 28, 34, 40, 46, 54, 60, 66, 72, 78, 84, 90 och 96 krävs fem gissningar. Resterande sextionio tal kräver sex gissningar eller fler, vilket innebär att det i snitt tar nästan sex gissningar att hitta rätt med den bästa strategin.

Om man istället ska gissa på ett tal mellan 1 och 1000 så har vi tio gånger så många alternativ till korrekt tal, men strategin kräver bara i snitt nio gissningar. Om vi har hundra gånger så många alternativ (ett tal mellan 1 och 10.000) krävs i snitt ungefär 12 gissningar, och för att gissa ett tal mellan 1 och 100.000 krävs knappt 16 gissningar.

Här är programmet i sin helhet (C#9):

using System;

const int max = 100;
const int correct = 22;
var steven = new NumberGuessingRobot(max);
var conny = new NumberThinkingRobot(correct, steven);
conny.Begin();

public class NumberThinkingRobot
{
    private readonly ICanGuess _guesser;
    private readonly int _correct;

    public NumberThinkingRobot(int correct, ICanGuess guesser)
    {
        _correct = correct;
        _guesser = guesser;
    }

    public void Begin()
    {
        var tryCount = 0;
        do
        {
            Console.WriteLine($"Try no. {++tryCount}");

            var guess = _guesser.Guess();

            if (guess < _correct)
                _guesser.TooSmall();
            else if (guess > _correct)
                _guesser.TooLarge();
            else
            {
                Console.WriteLine("Correct!");
                break;
            }
        } while (true);
    }
}

public class NumberGuessingRobot : ICanGuess
{
    private readonly int _max;
    private int _correct;
    private double _stepSize;

    public NumberGuessingRobot(int max)
    {
        _max = max;
        _correct = _max/2;
        _stepSize = _correct;
    }

    public int Guess()
    {
        Console.WriteLine($"Guess: {_correct}");
        return _correct;
    }

    public void TooSmall()
    {
        Console.WriteLine("Too small!");

        _stepSize /= 2.0;
        _stepSize = _stepSize < 1
            ? 1
            : _stepSize;

        _correct += (int)Math.Round(_stepSize);
        _correct = _correct > _max
            ? _max
            : _correct;
    }

    public void TooLarge()
    {
        Console.WriteLine("Too large!");

        _stepSize /= 2.0;
        _stepSize = _stepSize < 1
            ? 1
            : _stepSize;

        _correct -= (int)Math.Round(_stepSize);
        _correct = _correct < 1
            ? 1
            : _correct;
    }
}

public interface ICanGuess
{
    int Guess();
    void TooSmall();
    void TooLarge();
}

C# from the command window

If you are using .NET Core (or .NET 5.0) you can easily activate the ability to run C# code from the command window (Windows Terminal or the old cmd.exe).

To get started, install dotnet-script by typing:

dotnet tool install --global dotnet-script

Once installed, you can start the dotnet-script program just by typing:

dotnet-script

The prompt now looks like a greater than-sign (>). From here, you can type any C# code and you can reference anything within the System namespace. For example, if you type:

Console.WriteLine("Hello world!");

…you will get the following answer:

Hello world!

A few things to keep in mind: Types you add will be remembered for the whole session. For example, if you type in a class, you can use that class in the session. And if the command windows expect further input, the prompt will change from a greater than-signe to an asterisk sign (*). So if you type in something like class MyClass { and press Enter, nothing will be executed. Instead, the command window will indicate that more input is expected by displaying an asterisk sign. To try this out, type something like this (terminating each statement with Enter):

class MyClass {
public void DoSomething() {
Console.WriteLine("Hello!");
}
}

And then, you can use your new type like this:

new MyClass().DoSomething();

The response will be:

Hello!

The picture shows the different prompts when you type in a simple for loop:

This is a very powerful tool to use when you want to perform tasks that are a bit too complicated for a calculator, but perhaps a bit too simple fore an advanced computer program, or when you want to try out some API you are working on. Happy programming!

DOD är bättre OOD för tidskritiska system

Objektorienterad design (OOD) är ofta ett bra val för att det är relativt enkelt att använda med robusta mönster att följa. Men när man programmerar tidskritiskt, som t.ex. i när man gör spel, kan det vara värt att titta på dataorienterad design (DOD).

DOD handlar om att utnyttja processorns cache. Normalt när man läser och skriver data ber man processorn att läsa och skriva från RAM-minnet. Men när så sker, antar processorn att fler läs- och skrivoperationer kommer att ske med närliggande minneadresser, och cachar således närliggande data. Man kan alltså öka prestandan i sitt program, genom att se till att data som uppdateras ofta ligger nära varandra i minnet.

Jag har en väldigt enkel dator, en liten NUC från ASUS. Jag har skrivit ett litet testprogram i C#/.NET Core 3.1 som skapar 500 rymdskepp och flyttar dessa en halv miljon enheter i sidled, både enligt OOD och DOD.

Den objektorienterade lösningen

Denna kod definierar ett rymdskepp enligt traditionell OOD:

public class Spaceship
{
    public int X { get; set; }
    public int Y { get; set; }
}

Följande lilla kodsnutt skapar 500 rymdskepp i en array (givet att konstanten Ships är satt till 500):

var spaceshipsOop = new Spaceship[Ships];

for (var i = 0; i < Ships; i++)
    spaceshipsOop[i] = new Spaceship
    {
        X = 0,
        Y = i
    };

Och denna kod förflyttar alla 500 skepp en halv miljon enheter åt vänster (eftersom konstanten travelDistance är satt till 500 000):

for (var x = 0; x < travelDistance; x++)
    for (var i = 0; i < Ships; i++)
        spaceshipsOop[i].X++;

Hela kalaset kostar 194 millisekunder (3,124 sekunder i debugläge) för min lilla processor att utföra.

Den dataorienterade lösningen

I den dataorienterade lösningen vill vi försäkra oss om att data som kommer uppdateras samtidigt också finns lagrat tillsammans. Därför skulle definitionen av ett rymdskepp kunna bytas ut mot en definition av samtliga rymdskepp:

public class Spaceships
{
    public int[] X { get; set; }
    public int[] Y { get; set; }
}

Det innebär att koden som skapar 500 rymdskepp istället ser ut så här:

var spaceshipsDoa = new Spaceships
{
    X = new int[Ships], Y = new int[Ships]
};
for (var i = 0; i < Ships; i++)
{
    spaceshipsDoa.X[i] = 0;
    spaceshipsDoa.Y[i] = i;
}

Och slutligen, koden som förflyttar skeppen en halv miljon enheter åt vänster, ser nu ut så här:

for (var x = 0; x < travelDistance; x++)
    for (var i = 0; i < Ships; i++)
        spaceshipsDoa.X[i]++;

På min enhet har processorn fått jobba i 334 millisekunder (1,419 sekunder i debug-läge) för att åstadkomma detta, vilket är en rejäl försämring, men en förbättring på mer än 50 procent i debug-läge.

Vi kan alltså konstatera att man inte har någon automatisk vinst innan man kommer upp i lite tyngre arbeten, och det är därför fördelen med DOD var så tydligt i debug-läge. I release-läge ligger break-even för min del när 500 skepp gör en resa på två miljoner enheter (ungefär 1,2 sekunder oavsett strategi), och vid fem miljoner enheter kostar OOD-lösningen 3,2 sekunder och DOD-lösningen 2,5 sekunder.

Fördelen med DOD minskar ytterligare om antalet rymdskepp som ska förflyttas minskar, men ökar om antalet rymdskepp som ska förflyttas ökar. Med 2000 rymdskepp som ska som ska förflyttas fem miljoner enheter kostar OOD-lösningen 100 sekunder och DOD-lösningen 65 sekunder, vilket är en klart märkbar förbättring. Om en skälig mängd data ska användas väldigt intensivt och prestanda är kritiskt så är DOD att föredra, men i övrigt spelar det ingen större roll

Hela källkoden (C# version 8.0, .NET Core version 3.1):

using System;
using System.Diagnostics;

namespace ConsoleApp2
{
    public class Program
    {
        public const int Ships = 2000;

        private static void Main()
        {
            const int travelDistance = 5000000;

            var spaceshipsOop = new Spaceship[Ships];

            for (var i = 0; i < Ships; i++)
                spaceshipsOop[i] = new Spaceship
                {
                    X = 0,
                    Y = i
                };

            var stopwatch = new Stopwatch();
            stopwatch.Start();
            
            for (var x = 0; x < travelDistance; x++)
                for (var i = 0; i < Ships; i++)
                    spaceshipsOop[i].X++;

            stopwatch.Stop();

            Console.WriteLine($"Object oriented approach: {stopwatch.ElapsedMilliseconds}");

            stopwatch.Reset();

            var spaceshipsDoa = new Spaceships
            {
                X = new int[Ships], Y = new int[Ships]
            };
            for (var i = 0; i < Ships; i++)
            {
                spaceshipsDoa.X[i] = 0;
                spaceshipsDoa.Y[i] = i;
            }

            stopwatch.Start();

            for (var x = 0; x < travelDistance; x++)
                for (var i = 0; i < Ships; i++)
                    spaceshipsDoa.X[i]++;

            stopwatch.Stop();

            Console.WriteLine($"Data oriented approach: {stopwatch.ElapsedMilliseconds}");
        }
    }

    public class Spaceship
    {
        public int X { get; set; }
        public int Y { get; set; }
    }

    public class Spaceships
    {
        public int[] X { get; set; }
        public int[] Y { get; set; }
    }
}

Funktioner är andra klassens medborgare i C#

I C# 8 är fortfarande funktioner ett slags “andra klassens medborgare” i jämförelse med variabler. Man kan alltså fortfarande göra mer med en variabel än en funktion i C# 8.

Varken variabler eller funktioner kan skapas i namnrymder, båda kan skapas i en klass och båda kan skapas i en funktion, vilket illustreras av detta exempel:

namespace ConsoleApp1
{
    public class MyClass
    {
        public int VariableInClass;

        public void FunctionInClass()
        {
            int variableInFunction;
            
            void functionInFunction() { }
        }
    }
}

Eftersom det finns saker som du kan göra med variabler som du inte kan göra med funktioner, så är funktioner fortfarande att betrakta som ett slags andra klassens medborgare. Du kan inte lagra en funktion i en variabel, du kan inte skicka funktioner som argument och du kan inte använda type inference när du skapar funktioner.

var x = 10;

//Variabel som påverkar x
Action y = () => x++;
y();
Console.WriteLine(x);

//Funktion som påverkar x
void z() => x++;
z();
Console.WriteLine(x);

Att man inte kan lagra funktioner i variabler spelar inte någon jättestor roll. Som namnet antyder beskriver funktioner ofta något funktionellt, vilket i praktiken innebär att de innehåller programsatser. Programsatser kan även lagras i variabler, vilket innebär att funktionalitet kan lagras i en variabel och skickas som argument. Detta exempel visar hur instruktionen att summera två tal lagras i en variabel:

//Add the ability to add
//two integers in x.
Func<int, int, int> x =
    (int a, int b) => a + b;

//Execute the content
//of x, save the result in y.
var y = x(10, 20);

Detta exempel visar hur samma instruktion skickas som parameter till en funktion:

GiveMeFunctionality((a, b) => a + b);

C#-programmets startpunkt kan inte ersättas av en variabel, utan måste vara en riktig funktion kallad Main. I vissa fall kan man kringgå att en funktion inte kan användas som en variabel genom att skapa en variabel som anropar en funktion, och skicka den istället. T.ex. så här:

using System;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            GiveMeFunctionality(
                Functionality);
        }

        static int Functionality
            (int a, int b) =>
            a + b;
        
        static void GiveMeFunctionality(
            Func<int, int, int> x)
        {
            Console.WriteLine(x(10, 20));
        }
    }
}

Men till skillnad från variabler kan funktioner vara rekursiva. En funktion kan alltid anropas från en annan funktion, och tack vara stödet för reflection i C# (som t.ex. C++ saknar) kan funktioner alltid anropas oberoende av var i koden de är deklarerade.

var x = 0;
void i()
{
    x++;
    while (x < 10) 
        i();
}

Motsvarigheten skulle kunna vara något i den här stilen, vilket kräver att variabeln i finns medan den fortfarande deklareras:

var x = 0;
var i = () => {
    x++;
    while (x < 10) 
        i();
};

Pathfinding i terräng

A* (A-star) är en snabb pathfinder-algoritm som kan användas i t.ex. strategispel för att hitta vägar genom labyrinter eller terräng. Christoph Husse publicerade 2010 en briljant implementation i C# som tillåter egna kriterier, definierade i en solver, för kostnaden att flytta sig från en nod till en annan. I följande exempel har jag använt Christophs kod, men jag har skalat bort hans solver och istället hårdkodat in en viktad terräng. Kartan i exemplet är liten, endast 50×25 celler, och kostnaden för att gå på en cell sträcker sig från 0 till 99. De celler som har värdet 100 betraktas som ett oöverstigligt hinder. Bilden visar en körning, där startpositionen är längst ner till vänster.

Det hela utspelar sig i ett vanligt Windows Forms-fönster som har en karta (typen Map) och en stig (typen Path). Både Map och Path presenteras nedan. (De första fyra kodblocken visar alltså innehållet i fönstret, och de resterande kodblocken visar klasserna som används i projektet.)

private Map Map { get; set; }
private Path Path { get; set; }

I fönstrets Load-event skapar jag kartan, kartans terräng och använder sedan klassen som implementerar A* för att hitta den bästa vägen från start (längst ner till vänster) till mål (längst upp till höger). Funktionen FindPath ger null om det inte finns någon väg mellan punkterna.

private void Form1_Load(object sender, EventArgs e)
{
    Map = new Map(50, 25);
    CreateTerrain(Map);
    var aStar = new AStar(Map);
    Path = aStar.FindPath(
        new Point(0, Map.Height - 1),
        new Point(Map.Width - 1, 0))
        ?? new Path();
    Invalidate();
}

Funktionen för att generera terrängen, CreateTerrain, skapar ett godtyckligt bergslandskap.

private void CreateTerrain(Map map)
{
    var rnd = new Random();
    var walkcosts = new int[map.Width + 2, map.Height + 2];
    for (var y = 0; y < map.Height + 2; y++)
        for (var x = 0; x < map.Width + 2; x++)
            if (rnd.Next(0, 3) == 1)
                walkcosts[x, y] = rnd.Next(-10, 300);
    int GetAverage(int[,] a, int x, int y) =>
        (a[x - 1, y - 1] + a[x, y - 1] + a[x + 1, y - 1]
         + a[x - 1, y] + a[x, y] + a[x + 1, y]
         + a[x - 1, y + 1] + a[x, y + 1] + a[x + 1, y + 1]) / 9;
    for (var y = 0; y < map.Height; y++)
    {
        for (var x = 0; x < map.Width; x++)
        {
            var avg = GetAverage(walkcosts, x + 1, y + 1);
            if (avg < 0)
                avg = 0;
            else if (avg > 100)
                avg = 100;
            map.SetWalkCost(x, y, (byte)avg);
        }
    }
}

I fönstrets Paint-event ritas det hela ut på skärmen.

private void Form1_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.Clear(Color.Black);
    var m = new MapRenderer(Map, Path, 24);
    m.Draw(e.Graphics, 0, 0);
}

Klassen MapRenderer som ansvarar för ritandet ser ut så här:

using System.Drawing;
using System.Drawing.Drawing2D;

namespace PathFinderAStar
{
    public class MapRenderer
    {
        public Map Map { get; }
        public Path Path { get; }
        public int CellSize { get; }

        public MapRenderer(Map map, Path path, int cellSize)
        {
            Map = map;
            Path = path;
            CellSize = cellSize;
        }

        public void Draw(Graphics g, int offsetX, int offsetY)
        {
            var xpos = offsetX;
            var ypos = offsetY;
            using (var font = new Font(FontFamily.GenericSansSerif, 8))
            {
                for (var y = 0; y < Map.Height; y++)
                {
                    for (var x = 0; x < Map.Width; x++)
                    {
                        g.SmoothingMode = SmoothingMode.None;
                        g.DrawRectangle(
                            Pens.Gray,
                            xpos,
                            ypos,
                            CellSize,
                            CellSize);
                        using (var b = new SolidBrush(
                            Color.FromArgb(Map.GetWalkCost(x, y) * 2,
                            0,
                            0)))
                            g.FillRectangle(
                                b,
                                xpos + 2,
                                ypos + 2,
                                CellSize - 3,
                                CellSize - 3);
                        var pathCell = Path.GetCellAt(x, y);
                        g.SmoothingMode = SmoothingMode.HighQuality;
                        if (pathCell != null)
                            g.FillEllipse(
                                Brushes.CadetBlue,
                                xpos + 5,
                                ypos + 5,
                                CellSize - 9,
                                CellSize - 9);
                        g.DrawString(
                            Map.GetWalkCost(x, y).ToString(),
                            font,
                            Brushes.White,
                            xpos + 1,
                            ypos + 1);
                        xpos += CellSize;
                    }
                    xpos = offsetX;
                    ypos += CellSize;
                }
            }
        }
    }
}

Klassen Map beskriver en 2-dimensionell karta, på vilken vi ska hitta en stig.

using System;
using System.Diagnostics;
using System.Drawing;

namespace PathFinderAStar
{
    public class Map
    {
        private readonly MapCell[,] _terrain;
        public int Width { get; }
        public int Height { get; }

        public Map(int width, int height)
        {
            if (width < 2 || width > 2000)
                throw new ArgumentOutOfRangeException(nameof(width));
            if (height < 2 || height > 2000)
                throw new ArgumentOutOfRangeException(nameof(height));
            Width = width;
            Height = height;
            _terrain = new MapCell[width, height];
        }

        public byte GetWalkCost(int x, int y)
        {
            if (x < 0 || x >= Width)
                throw new ArgumentOutOfRangeException(nameof(x));
            if (y < 0 || y >= Height)
                throw new ArgumentOutOfRangeException(nameof(y));
            var cell = _terrain[x, y];
            if (cell == null)
                return 0;
            Debug.Assert(cell.X == x);
            Debug.Assert(cell.Y == y);
            return cell.WalkCost;
        }

        public void SetWalkCost(int x, int y, byte walkCost)
        {
            if (x < 0 || x >= Width)
                throw new ArgumentOutOfRangeException(nameof(x));
            if (y < 0 || y >= Height)
                throw new ArgumentOutOfRangeException(nameof(y));
            if (walkCost > 100)
                throw new ArgumentOutOfRangeException(nameof(walkCost));
            var cell = _terrain[x, y];
            if (cell == null)
            {
                cell = new MapCell(x, y, walkCost);
                _terrain[x, y] = cell;
                return;
            }
            Debug.Assert(cell.X == x);
            Debug.Assert(cell.Y == y);
            cell.WalkCost = walkCost;
        }

        public void Clear()
        {
            for (var y = 0; y < Height; y++)
                for (var x = 0; x < Width; x++)
                    _terrain[x, y] = null;
        }

        public MapCell GetCell(int x, int y) =>
            GetCell(new Point(x, y));

        public MapCell GetCell(Point p)
        {
            if (p.X < 0 || p.X >= Width || p.Y < 0 || p.Y >= Height)
                return null;
            var c = _terrain[p.X, p.Y];
            return c;
        }

        public double GetDistance(MapCell start, MapCell goal)
        {
            var actualDistance = Math.Sqrt((start.X - goal.X)
                * (start.X - goal.X)
                + (start.Y - goal.Y)
                * (start.Y - goal.Y));
            return actualDistance + (double)goal.WalkCost / 10;
        }

        public void Add(MapCell cell)
        {
            if (cell == null)
                throw new ArgumentNullException(nameof(cell));
            if (cell.X < 0 || cell.X >= Width)
                throw new ArgumentOutOfRangeException(nameof(cell.X));
            if (cell.Y < 0 || cell.Y >= Height)
                throw new ArgumentOutOfRangeException(nameof(cell.Y));
            if (_terrain[cell.X, cell.Y] != null)
                throw new SystemException();
            _terrain[cell.X, cell.Y] = cell;
        }

        public void Remove(MapCell cell)
        {
            if (cell == null)
                throw new ArgumentNullException(nameof(cell));
            if (_terrain[cell.X, cell.Y] == null)
                throw new SystemException();
            if (_terrain[cell.X, cell.Y] != cell)
                throw new SystemException();
            _terrain[cell.X, cell.Y] = null;
        }

        public bool Contains(MapCell cell) =>
            _terrain[cell.X, cell.Y] != null;

        public void SetWithHistory(int x, int y, MapCell cell) =>
            _terrain[x, y] = cell;

        public bool IsEmpty
        {
            get
            {
                for (var y = 0; y < Height; y++)
                    for (var x = 0; x < Width; x++)
                        if (_terrain[x, y] != null)
                            return false;
                return true;
            }
        }

        public int CompareCells(MapCell c1, MapCell c2)
        {
            if (c1 == null)
                throw new ArgumentNullException(nameof(c1));
            if (c2 == null)
                throw new ArgumentNullException(nameof(c2));
            if (c1.F < c2.F)
                return -1;
            if (c1.F > c2.F)
                return 1;
            return 0;
        }

    }
}

Stigen beskrivs av klassen Path:

using System.Collections.Generic;
using System.Linq;

namespace PathFinderAStar
{
    public class Path : List<MapCell>
    {
        public Path()
        {
        }

        public Path(int count)
        {
            for (var i = 0; i < count; i++)
                Add(null);
        }

        public MapCell GetCellAt(int x, int y) =>
            this.FirstOrDefault(c => c.X == x && c.Y == y);
    }
}

Både kartan (Map) och stigen (Path) består av referenser till celler som beskriver hur terrängen ser ut på en specifik plats. Typen heter MapCell (G, H, F och Index används av A*.):

namespace PathFinderAStar
{
    public class MapCell
    {
        private byte _walkCost;
        public int X { get; }
        public int Y { get; }
        public double G { get; set; }
        public double H { get; set; }
        public double F { get; set; }
        public int Index { get; set; }

        public byte WalkCost
        {
            get => _walkCost;
            set => _walkCost = value > 100 ? (byte)100 : value;
        }

        public bool IsWall =>
            WalkCost >= 100;

        public MapCell(int x, int y, byte walkCost)
        {
            X = x;
            Y = y;
            WalkCost = walkCost;
        }
    }
}

Sen har vi själva A*-algoritmen, såsom den implementerats av Christoph Husse, med mina förenklingar. Den konsumeras direkt i formulärets Load-event och ligger i klassen AStar.

using System.Drawing;

namespace PathFinderAStar
{
    public class AStar
    {
        private Map Map { get; }
        private Map ClosedSet { get; }
        private Map OpenSet { get; }
        private Map CameFrom { get; }
        private Map RuntimeGrid { get; }
        private PriorityQueue OrderedOpenSet { get; }

        public AStar(Map map)
        {
            Map = map;
            ClosedSet = new Map(Map.Width, Map.Height);
            OpenSet = new Map(Map.Width, Map.Height);
            CameFrom = new Map(Map.Width, Map.Height);
            RuntimeGrid = new Map(Map.Width, Map.Height);
            OrderedOpenSet = new PriorityQueue(Map);
        }

        public Path FindPath(Point start, Point goal)
        {
            var startCell = Map.GetCell(start);
            var goalCell = Map.GetCell(goal);
            if (startCell == goalCell)
                return new Path { startCell };
            ClosedSet.Clear();
            OpenSet.Clear();
            CameFrom.Clear();
            RuntimeGrid.Clear();
            OrderedOpenSet.Clear();
            startCell.G = 0.0;
            startCell.H = Map.GetDistance(startCell, goalCell);
            startCell.F = startCell.H;
            OpenSet.Add(startCell);
            OrderedOpenSet.Push(startCell);
            RuntimeGrid.Add(startCell);
            var neighbours = new Path(8);
            while (!OpenSet.IsEmpty)
            {
                var next = OrderedOpenSet.Pop();
                if (next == goalCell)
                {
                    var result = ReconstructPath(
                        CameFrom,
                        CameFrom.GetCell(goalCell.X, goalCell.Y));
                    result.Add(goalCell);
                    return result;
                }
                OpenSet.Remove(next);
                ClosedSet.Add(next);
                StoreNeighbours(next, neighbours);
                foreach (var currentNeighbour in neighbours)
                {
                    if (currentNeighbour == null
                        || currentNeighbour.IsWall
                        || ClosedSet.Contains(currentNeighbour))
                        continue;
                    var tentativeG = RuntimeGrid.GetCell(next.X, next.Y).G
                        + Map.GetDistance(next, currentNeighbour);
                    if (!TentativeIsBetter(currentNeighbour,
                        tentativeG, out var added))
                        continue;
                    CameFrom.SetWithHistory(
                        currentNeighbour.X,
                        currentNeighbour.Y,
                       next);
                    if (!RuntimeGrid.Contains(currentNeighbour))
                        RuntimeGrid.Add(currentNeighbour);
                    currentNeighbour.G = tentativeG;
                    currentNeighbour.H = Map.GetDistance(
                        currentNeighbour,
                        goalCell);
                    currentNeighbour.F = currentNeighbour.G
                        + currentNeighbour.H;
                    if (added)
                        OrderedOpenSet.Push(currentNeighbour);
                    else
                        OrderedOpenSet.Update(currentNeighbour);
                }

            }
            return null;
        }

        private bool TentativeIsBetter(
            MapCell currentNeighbour,
            double tentativeG,
            out bool added)
        {
            if (!OpenSet.Contains(currentNeighbour))
            {
                OpenSet.Add(currentNeighbour);
                added = true;
                return true;
            }
            added = false;
            return tentativeG < RuntimeGrid.GetCell(
                currentNeighbour.X,
                currentNeighbour.Y).G;
        }

        private void StoreNeighbours(MapCell c, Path neighbours)
        {
            neighbours[0] = Map.GetCell(c.X - 1, c.Y - 1);
            neighbours[1] = Map.GetCell(c.X, c.Y - 1);
            neighbours[2] = Map.GetCell(c.X + 1, c.Y - 1);
            neighbours[3] = Map.GetCell(c.X - 1, c.Y);
            neighbours[4] = Map.GetCell(c.X + 1, c.Y);
            neighbours[5] = Map.GetCell(c.X - 1, c.Y + 1);
            neighbours[6] = Map.GetCell(c.X, c.Y + 1);
            neighbours[7] = Map.GetCell(c.X + 1, c.Y + 1);
        }

        private Path ReconstructPath(Map cameFrom, MapCell currentCell)
        {
            void ReconstructPathRecursive(
                Map recursiveCameFrom,
                MapCell recursiveCurrentCell,
                Path result)
            {
                var item = recursiveCameFrom.GetCell(
                    recursiveCurrentCell.X,
                    recursiveCurrentCell.Y);
                if (item != null)
                    ReconstructPathRecursive(
                        recursiveCameFrom,
                        item,
                        result);
                result.Add(recursiveCurrentCell);

            }
            var path = new Path();
            ReconstructPathRecursive(cameFrom, currentCell, path);
            return path;
        }
    }
}

Och slutligen, typen PriorityQueue som algoritmen använder för att hålla reda på vilka celler som undersöks, ser ut så här:

using System.Collections.Generic;

namespace PathFinderAStar
{
    public class PriorityQueue
    {
        private List<MapCell> InnerList { get; } = new List<MapCell>();
        private Map Map { get; }

        public PriorityQueue(Map map)
        {
            Map = map;
        }

        public void Clear() =>
            InnerList.Clear();

        public int Push(MapCell c)
        {
            var index1 = InnerList.Count;
            c.Index = index1;
            InnerList.Add(c);
            do
            {
                if (index1 <= 0)
                    break;
                var index2 = (index1 - 1) / 2;
                if (Compare(index1, index2) < 0)
                {
                    SwitchElements(index1, index2);
                    index1 = index2;
                    continue;
                }
                break;

            } while (true);
            return index1;
        }

        public MapCell Pop()
        {
            var result = InnerList[0];
            InnerList[0] = InnerList[InnerList.Count - 1];
            InnerList[0].Index = 0;
            InnerList.RemoveAt(InnerList.Count - 1);
            result.Index = -1;
            var p = 0;
            do
            {
                var pn = p;
                var p1 = 2 * p + 1;
                var p2 = 2 * p + 2;
                if (InnerList.Count > p1 && Compare(p, p1) > 0)
                    p = p1;
                if (InnerList.Count > p2 && Compare(p, p2) > 0)
                    p = p2;
                if (p == pn)
                    break;
                SwitchElements(p, pn);
            } while (true);
            return result;
        }

        public void Update(MapCell cell)
        {
            var count = InnerList.Count;
            while (cell.Index - 1 > 0
                && Compare(cell.Index - 1, cell.Index) > 0)
                SwitchElements(cell.Index - 1, cell.Index);
            while (cell.Index + 1 < count
                && Compare(cell.Index + 1, cell.Index) < 0)
                SwitchElements(cell.Index + 1, cell.Index);
        }

        private int Compare(int i1, int i2) =>
            Map.CompareCells(InnerList[i1], InnerList[i2]);

        private void SwitchElements(int i1, int i2)
        {
            var c1 = InnerList[i1];
            InnerList[i1] = InnerList[i2];
            InnerList[i2] = c1;
            InnerList[i1].Index = i1;
            InnerList[i2].Index = i2;
        }
    }
}

Med denna kod på plats har man en enkel funktion (FindPath) att anropa för att hitta en stig mellan två punkter (om sådan finns). Och om man vill anpassa kriterierna för hur en stig väljs, t.ex. hur terrängen ska viktas mot avståndet, görs det i funktionen GetDistance i klassen Map.

IMDb Scraper

IMDb Scraper is a simple library for extracting a movie title and year from a IMDb ID.

Installation (.NET Framework 4.6):

Install-Package ImdbScraper

Example:

var repository = new Repository();
var result = repository.GetMovie(87332);
Console.WriteLine(result.ToString());

 

Fem inlägg om C# på Nethouse-bloggen

Jag har skrivit fem inlägg om C# på Nethouse blogg som jag tänkte be att få dela med mig av.

Kortare kod med mönstermatchning i C# 7

Förenklad hantering av funktioner som producerar multipla värden i C#

Skalning i MonoGame

Late binding i C#

Lazy evaluated string interpolation

Uppdatering 2017-09-22:

Kort kod och syftningsfel

Uppdatering 2018-01-26:

Microsoft Cognitive Services: Computer Vision

Entity Framework 6

Entity Framework 6 (EF6) är mycket enkelt att komma igång med. För att få tillgång till aktuell version, använd Nuget-paketet EntityFramework. Den här bloggposten visar det minsta man behöver veta för att komma igång med EF6 i sitt program.

Install-Package EntityFramework

Dessutom behöver du ha en tom databas att jobba mot. I detta exempel antar jag att databasen heter EfTest. Anslutningssträngen lagras i konfigurationsfilen. I mitt fall är det en vanlig Console Application.

  <connectionStrings>
    <add name="EfTest" providerName="System.Data.SqlClient" connectionString="Data Source=XXX;Initial Catalog=EfTest;Integrated Security=True" />
  </connectionStrings>

Nästa steg är att skapa DbContext-objektet. Denna ärver från System.Data.Entity.DbContext. Basklassen behöver veta vilken anslutningssträng som ska användas. Antingen skickar man in själva anslutningssträngen, eller så hänvisar man till ett namn. De tabeller som ska användas beskrivs av klasser och väljs in som argument till den generiska DbSet (också i System.Data.Entity). Här har jag helt enkelt skapat två klasser som heter Tabell1 och Tabell2.

    public class TestContext : DbContext
    {
        public TestContext() : base("name=EfTest")
        {
        }
        public DbSet<Tabell1> Tabell1 { get; set; }
        public DbSet<Tabell2> Tabell2 { get; set; }
    }

Låt oss titta på klassen Tabell1. Den innehåller en primärnyckel (int) och en sträng som heter Value. Attributet Key på fältet (System.ComponentModel.DataAnnotations) anger att motsvarande kolumn i databasen är en primärnyckel. Attributet DatabaseGenerated (System.ComponentModel.DataAnnotations.Schema) använder jag här för att säga att kolumnen ska vara en räknare, och alltså inte något som jag ska tillhandahålla data till vid tillägg. Det innebär alltså att nya poster skapas genom att Value får ett värde, inte Tabell1Id.

    using (var db = new TestContext())
    {
        db.Tabell1.Add(new Tabell1 {Value = "Hej"});
        db.SaveChanges();
    }

När tabellen läses av, kan man konstatera att nyckelkolumnen i detta fall ska ha fått sitt värde ändå.

    using (var db = new TestContext())
    {
        foreach (var tabell1 in db.Tabell1)
        {
            Console.WriteLine($"{tabell1.Tabell1Id} - {tabell1.Value}");
        }
    }

Det är när programmet körs första gången som databasen byggs upp automatiskt. Om en befintlig tabell förändras, t.ex. genom att klassen Tabell1 får en till property (vilket motsvarar en till kolumn) kommer programmet inte längre att fungera. Då måste en strategi för uppgradering tillhandahållas. T.ex. kan man säga att databasschemat helt enkelt ska genereras om vid förändring. Det innebär att man förlorar allt data, så med denna strategi vill man förmodligen komplettera med kod som lägger in lite testdata.

Database.SetInitializer(new DropCreateDatabaseIfModelChanges<TestContext>());

För att tillhandahålla testdata, skriv en egen initializer, baserad på den som du vill använda, och tillhandahåll testdata i funktionen Seed.

    public class DropCreateInitializer : DropCreateDatabaseIfModelChanges<TestContext>
    {
        protected override void Seed(TestContext context)
        {
            context.Tabell1.Add(new Tabell1 { Value = "Hej" });
            context.Tabell1.Add(new Tabell1 { Value = "Oj" });
        }
    }

Sen är det bara att använda sin egna initializer.

Database.SetInitializer(new DropCreateInitializer());

Fullskärm med exakt virtuell bildupplösning

Om du vill bygga ett spel med Monogame som 1) körs i fullsärmsläge och 2) har en låg virtuell upplösning, typ 320 x 200 pixlar, så finns en enkel lösning. Istället för att rendera spelet på din backbuffer, så renderar du till en render target som är just 320 x 200 pixlar stor. För att åstadkomma detta, skapa en ny property i ditt spel, av typen RenderTarget2D.

private RenderTarget2D renderTarget { get; set; }

I konstruktorn, givet att vi befinner oss i release-kompileringen, skapar man en backbuffer som är lika stor som skärmen.

            graphics = new GraphicsDeviceManager(this);
#if DEBUG
            ScreenWidth = 640;
            ScreenHeight = 400;
            graphics.PreferredBackBufferWidth = ScreenWidth;
            graphics.PreferredBackBufferHeight = ScreenHeight;
            graphics.IsFullScreen = false;
#else
            ScreenWidth = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width;
            ScreenHeight = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height;
            graphics.PreferredBackBufferWidth = ScreenWidth;
            graphics.PreferredBackBufferHeight = ScreenHeight;
            graphics.IsFullScreen = true;
#endif
            Content.RootDirectory = "Content";

Funktionen Initialize är en lämplig plats att skapa RenderTarget2D-objektet. Det är storleken på den som styr den virtuella upplösningen.

renderTarget = new RenderTarget2D(GraphicsDevice, 320, 200);

Slutligen renderas spelet mot RenderTarget2D-objektet. Funktionen Draw skulle kunna se ut så här:

protected override void Draw(GameTime gameTime)
{
    graphics.GraphicsDevice.SetRenderTarget(renderTarget);
    graphics.GraphicsDevice.Clear(Color.Black);

    //Rita spelet här!
    spriteBatch.Begin();
    spriteBatch.Draw(hej, new Vector2(0, 0), Color.White);
    spriteBatch.Draw(hej, new Vector2(310, 190), Color.White);
    spriteBatch.End();

    graphics.GraphicsDevice.SetRenderTarget(null);
    spriteBatch.Begin(samplerState: SamplerState.PointClamp);
    spriteBatch.Draw(renderTarget, new Rectangle(0, 0, ScreenWidth, ScreenHeight), new Rectangle(0, 0, 320, 200), Color.White);
    spriteBatch.End();
    base.Draw(gameTime);
}

Notera det sista anropet på spriteBatch.Draw, den tredje sista raden. Den skalar upp spelet till den riktiga back bufferten. Om man vill ha rektangulära pixlar, ska det hanteras där. Denna exempelkod tar inte hänsyn till olika bildförhållanden.

Så här ser hela koden ut:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace Game1
{
    public class Game1 : Game
    {
        private RenderTarget2D renderTarget { get; set; }
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        private int ScreenWidth { get; }
        private int ScreenHeight { get; }
        private Texture2D hej;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
#if DEBUG
            ScreenWidth = 640;
            ScreenHeight = 400;
            graphics.PreferredBackBufferWidth = ScreenWidth;
            graphics.PreferredBackBufferHeight = ScreenHeight;
            graphics.IsFullScreen = false;
#else
            ScreenWidth = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width;
            ScreenHeight = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height;
            graphics.PreferredBackBufferWidth = ScreenWidth;
            graphics.PreferredBackBufferHeight = ScreenHeight;
            graphics.IsFullScreen = true;
#endif
            Content.RootDirectory = "Content";
        }
        protected override void Initialize()
        {
            renderTarget = new RenderTarget2D(GraphicsDevice, 320, 200);
            base.Initialize();
        }
        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            hej = Content.Load<Texture2D>("hej");
        }
        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();
            base.Update(gameTime);
        }
        protected override void Draw(GameTime gameTime)
        {
            graphics.GraphicsDevice.SetRenderTarget(renderTarget);
            graphics.GraphicsDevice.Clear(Color.Black);

            //Rita spelet här!
            spriteBatch.Begin();
            spriteBatch.Draw(hej, new Vector2(0, 0), Color.White);
            spriteBatch.Draw(hej, new Vector2(310, 190), Color.White);
            spriteBatch.End();

            graphics.GraphicsDevice.SetRenderTarget(null);
            spriteBatch.Begin(samplerState: SamplerState.PointClamp);
            spriteBatch.Draw(renderTarget, new Rectangle(0, 0, ScreenWidth, ScreenHeight), new Rectangle(0, 0, 320, 200), Color.White);
            spriteBatch.End();
            base.Draw(gameTime);
        }
    }
}

Jag utvecklar detta på min arbetsgivares blogg.

Skala upp Monogame

När man bygger retrospel med en upplösning på omkring 320 x 200 pixlar, vill man gärna skala upp spelet så att sprites och andra objekt inte blir så små på en modern skärm. För den som programmerar Monogame finns flera lösningar. Jag tycker att den enklaste metoden är att använda ett transform matrix. När jag debuggar mitt spel, tycker jag att en uppskalning på 3 x 3 är rätt lämplig, vilket innebär att ett fönster på 960 x 600 pixlar är lämpligt. Detta är spelets konstruktor, spelet har kompletterats med en medlemsvariabel vid namn transformMatrix av typen Matrix.

graphics.PreferredBackBufferWidth = 960;
graphics.PreferredBackBufferHeight = 600;
graphics.IsFullScreen = false;
transformMatrix = Matrix.CreateScale(960 / 320, 600 / 200, 1.0f);

I skarpt läge låter jag skärmupplösningen styra skalningen, eftersom jag vill att spelet ska köras i fullskärm.

var width = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width;
var height = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height;
graphics.PreferredBackBufferWidth = width;
graphics.PreferredBackBufferHeight = height;
graphics.IsFullScreen = true;
transformMatrix = Matrix.CreateScale(width / 320, height / 200, 1.0f);

För att effekten ska slå igenom, ska Matrix-objektet skickas med till SpriteBatch-objektets Begin-metod. För att undvika oönskad kantutjämning använder jag en sampler state som heter PointClamp.

spriteBatch.Begin(transformMatrix: transformMatrix, samplerState: SamplerState.PointClamp);

Ett exempelspel skulle kunna se ut så här:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace Game1
{
    public class Game1 : Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        Matrix transformMatrix;
        Texture2D testtexture { get; set; }
        private int x = 0;
        private int y = 0;
        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
#if DEBUG
            graphics.PreferredBackBufferWidth = 960;
            graphics.PreferredBackBufferHeight = 600;
            graphics.IsFullScreen = false;
            transformMatrix = Matrix.CreateScale(960 / 320, 600 / 200, 1.0f);
#else
            var width = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width;
            var height = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height;
            graphics.PreferredBackBufferWidth = width;
            graphics.PreferredBackBufferHeight = height;
            graphics.IsFullScreen = true;
            transformMatrix = Matrix.CreateScale(width / 320, height / 200, 1.0f);
#endif
            Content.RootDirectory = "Content";
        }
        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            testtexture = Content.Load<Texture2D>("my_folder");
        }
        protected override void Update(GameTime gameTime)
        {
            var state = Keyboard.GetState();
            if (state.IsKeyDown(Keys.Escape))
                Exit();
            if (state.IsKeyDown(Keys.Left))
                x--;
            else if (state.IsKeyDown(Keys.Right))
                x++;
            if (state.IsKeyDown(Keys.Up))
                y--;
            else if (state.IsKeyDown(Keys.Down))
                y++;
            base.Update(gameTime);
        }
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);
            spriteBatch.Begin(transformMatrix: transformMatrix, samplerState: SamplerState.PointClamp);
            spriteBatch.Draw(testtexture, new Vector2(x, y), Color.White);
            spriteBatch.End();
            base.Draw(gameTime);
        }
    }
}

Ett problem som man bör känna till, är att om bildförhållandet mellan den virtuella upplösningen och den fysiska upplösningen inte är samma, så kommer fler pixlar än önskat att synas. När man kör i fönster, så kan man styra detta så att bildförhållandet är detsamma. 960 x 600 har samma bildförhållande som 320 x 200. Men i fullskärmsläge bör man ha detta i åtanke.

Jag utvecklar detta på min arbetsgivares blogg.

Labyrint (recursive backtracking)

Jag har börjat arbeta med ett datorspel i kategorin roguelike. Varje spel ska vara unikt, så kartan genereras när en användare spelar spelet. Vid uppstart skapar jag en labyrint som sen används som kontur för rummen som genereras efter behov. De rum som spelaren inte besöker behöver man trots allt inte hålla i RAM, men jag vill ha konturen färdig för att kunna ha lite kontroll över kartans kvalité.

 
Totalt okunnig om labyrintalgoritmer läste jag igenom Wikipedias artikel om recursive backtracking som i korthet går ut på följande:
1. Utgå från den cell som ska vara startpunkten, C.
 
2. Om C inte är vald sedan tidigare, plocka bort en slumpvis utvald vägg på C. Följ den nyskapade öppningen och låt den vara C.
 
3. Om nya C har varit C tidigare, stega bakåt till första cell som fortfarande har fyra väggar, gör den till C, upprepa steg 2.
 
4. Om C nu är samma som var C vid punkt 1 så är labyrinten klar.
 
Så i princip startar mitt (blivande) datorspel med en hel hög av celler (precis hur många jag vill) som håller information om in- och utgångar (konturen), men det är först när cellen används som fyller jag på med s.k. tiles, alltså de byggstenar som bestämmer kartans detaljer. Låt oss titta på själva labyrinten, och lämna resten därhän.
För detta exempel valde jag språket C#. Trots att språket är lite pratigt, är det ganska flexibelt och användbart.

Först av allt behövs en entitet – en cell – som kan ha väggar i fyra riktningar: Norr, öst, söder och väst. Jag anger att jag både vill kunna läsa (get) och skriva (set) dessa egenskaper under programmets gång. Initialt måste cellen ha alla sina fyra möjliga väggar.

public class Cell
{
    public bool WallNorth { get; set; } = true;
    public bool WallEast { get; set; } = true;
    public bool WallSouth { get; set; } = true;
    public bool WallWest { get; set; } = true;
}

Vidare måste cellen kunna svara på var i labyrinten den befinner sig. Det behövs för att cellens grannar ska kunna hittas. En cell som inte kan svara på det, får inte skapas över huvudet taget.

    public int X { get; }
    public int Y { get; }
    public Cell(int x, int y)
    {
        X = x;
        Y = y;
    }

Eftersom cellen enligt algoritmens regler måste kunna svara på hur många väggar den har, måste vi beskriva hur vi kan veta det.

    public int WallsCount
    {
        get
        {
            var c = WallNorth ? 1 : 0;
            c += WallEast ? 1 : 0;
            c += WallSouth ? 1 : 0;
            c += WallWest ? 1 : 0;
            return c;
        }
    }

Och slutligen måste cellen kunna berätta om sin position i ett lämpligt format.

    public Point Position => new Point(X, Y);

Därefter behövs en representation av själva labyrinten som kan hålla en matris av celler. Denna gång väljer jag att skapa en liten labyrint med 100 celler (10 x 10). Bredden och höjden måste kunna läsas (get) under programmets gång. Koden skapar inte cellerna, endast platshållaren för dem.

public class Labyrinth
{
    public static int Width { get; } = 10;
    public static int Height { get; } = 10;
    public Cell[,] Cell = new Cell[Width, Height];
}

Sen ska vi beskriva algoritmen i labyrintens konstruktor (=funktion med ansvar för initiering). Vi börjar med att berätta att vi vill kunna skapa slumptal, och att platshållaren för våra celler verkligen ska innehålla celler.

        var rnd = new Random();
        for (var y = 0; y < Height; y++)
            for (var x = 0; x < Width; x++)
                Cells[x, y] = new Cell(x, y);

Därefter måste vi definiera konceptet med grannar för konstruktorn. Grannar är angränsande celler.

        Func<int, int, List<Point>> getNeighbours = (x, y) =>
        {
            var ret = new List<Point>();
            if (y > 0 && Cells[x, y - 1].WallsCount >= 4)
                ret.Add(Cells[x, y - 1].Position);
            if (x < Width - 1 && Cells[x + 1, y].WallsCount >= 4)
                ret.Add(Cells[x + 1, y].Position);
            if (y < Height - 1 && Cells[x, y + 1].WallsCount >= 4)
                ret.Add(Cells[x, y + 1].Position);
            if (x > 0 && Cells[x - 1, y].WallsCount >= 4)
                ret.Add(Cells[x - 1, y].Position);
            return ret;
        };

Sen måste vi beskriva hur man slår in en vägg. Det är inte så enkelt som att sätta en cells vägg-egenskap (t.ex. WallNorth) till false, eftersom grannen åt det hållet också måste få sin motsvarande vägg (t.ex. WallSouth) satt till false.

        Action<Cell, Point> knockWall = (cell1, cell2) =>
        {
            if (cell2.Y < cell1.Y)
            {
                cell1.WallNorth = false;
                Cells[cell2.X, cell2.Y].WallSouth = false;
                return;
            }
            if (cell2.X > cell1.X)
            {
                cell1.WallEast = false;
                Cells[cell2.X, cell2.Y].WallWest = false;
                return;
            }
            if (cell2.Y > cell1.Y)
            {
                cell1.WallSouth = false;
                Cells[cell2.X, cell2.Y].WallNorth = false;
                return;
            }
            if (cell2.X < cell1.X)
            {
                cell1.WallWest = false;
                Cells[cell2.X, cell2.Y].WallEast = false;
                return;
            }
        };

Nu har konstruktorn tillgång till resurserna, och den vet vad en granne är och hur man slår in en vägg. Dags att sätta igång! Vi behöver hålla koll på vilka celler vi besökt…

            var queue = new Queue<Cell>();

…och så behöver vi en slumpvis utvald cell att börja i.

            var c = Cells[rnd.Next(Width), rnd.Next(Height)];
            queue.Enqueue(c);

Därefter, slå ut en vägg och följ efter. Om det inte går, backa till start och prova igen.

            while (queue.Count > 0)
            {
                var neighbours = getNeighbours(c.X, c.Y);
                if (neighbours.Count > 0)
                {
                    var neighbour = neighbours[rnd.Next(neighbours.Count)];
                    knockWall(c, neighbour);
                    queue.Enqueue(c);
                    c = Cells[neighbour.X, neighbour.Y];
                }
                else
                {
                    c = queue.Dequeue();
                }
            }

 
För att representera detta på skärmen, behöver jag bara bekymra mig om två av väggarna, höger och under. Högra grannens vänstervägg och undre grannens övre vägg har samma status (tack vare knockWall).
    private void Form1_Paint(object sender, PaintEventArgs e)
    {
        const int size = 20;
        for (var y = 0; y < Labyrinth.Height; y++)
            for (var x = 0; x < Labyrinth.Width; x++)
            {
                if (Labyrinth.Cells[x, y].WallEast)
                    e.Graphics.DrawLine(Pens.Black, x * size + (size - 1), y * size, x * size + (size - 1), y * size + (size - 1));
                if (Labyrinth.Cells[x, y].WallSouth)
                    e.Graphics.DrawLine(Pens.Black, x * size, y * size + (size - 1), x * size + (size - 1), y * size + (size - 1));
            }
    }
 
Hela källkoden ser ut så här:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        private Labyrinth Labyrinth { get; } = new Labyrinth();
        public Form1()
        {
            InitializeComponent();
        }

    private void Form1_Paint(object sender, PaintEventArgs e)
    {
        const int size = 20;
        for (var y = 0; y < Labyrinth.Height; y++)
            for (var x = 0; x < Labyrinth.Width; x++)
            {
                if (Labyrinth.Cells[x, y].WallEast)
                    e.Graphics.DrawLine(Pens.Black, x * size + (size - 1), y * size, x * size + (size - 1), y * size + (size - 1));
                if (Labyrinth.Cells[x, y].WallSouth)
                    e.Graphics.DrawLine(Pens.Black, x * size, y * size + (size - 1), x * size + (size - 1), y * size + (size - 1));
            }
    }
    }
    public class Cell
    {
        public bool WallNorth { get; set; } = true;
        public bool WallEast { get; set; } = true;
        public bool WallSouth { get; set; } = true;
        public bool WallWest { get; set; } = true;
        public int X { get; }
        public int Y { get; }
        public Cell(int x, int y)
        {
            X = x;
            Y = y;
        }
        public int WallsCount
        {
            get
            {
                var c = WallNorth ? 1 : 0;
                c += WallEast ? 1 : 0;
                c += WallSouth ? 1 : 0;
                c += WallWest ? 1 : 0;
                return c;
            }
        }
        public Point Position => new Point(X, Y);
    }
    public class Labyrinth
    {
        public static int Width { get; } = 10;
        public static int Height { get; } = 10;
        public Cell[,] Cells = new Cell[Width, Height];
        public Labyrinth()
        {
            var rnd = new Random();
            for (var y = 0; y < Height; y++)
                for (var x = 0; x < Width; x++)
                    Cells[x, y] = new Cell(x, y);
            Func<int, int, List<Point>> getNeighbours = (x, y) =>
            {
                var ret = new List<Point>();
                if (y > 0 && Cells[x, y - 1].WallsCount >= 4)
                    ret.Add(Cells[x, y - 1].Position);
                if (x < Width - 1 && Cells[x + 1, y].WallsCount >= 4)
                    ret.Add(Cells[x + 1, y].Position);
                if (y < Height - 1 && Cells[x, y + 1].WallsCount >= 4)
                    ret.Add(Cells[x, y + 1].Position);
                if (x > 0 && Cells[x - 1, y].WallsCount >= 4)
                    ret.Add(Cells[x - 1, y].Position);
                return ret;
            };
            Action<Cell, Point> knockWall = (cell1, cell2) =>
            {
                if (cell2.Y < cell1.Y)
                {
                    cell1.WallNorth = false;
                    Cells[cell2.X, cell2.Y].WallSouth = false;
                    return;
                }
                if (cell2.X > cell1.X)
                {
                    cell1.WallEast = false;
                    Cells[cell2.X, cell2.Y].WallWest = false;
                    return;
                }
                if (cell2.Y > cell1.Y)
                {
                    cell1.WallSouth = false;
                    Cells[cell2.X, cell2.Y].WallNorth = false;
                    return;
                }
                if (cell2.X < cell1.X)
                {
                    cell1.WallWest = false;
                    Cells[cell2.X, cell2.Y].WallEast = false;
                    return;
                }
            };
            var queue = new Queue<Cell>();
            var c = Cells[rnd.Next(Width), rnd.Next(Height)];
            queue.Enqueue(c);
            while (queue.Count > 0)
            {
                var neighbours = getNeighbours(c.X, c.Y);
                if (neighbours.Count > 0)
                {
                    var neighbour = neighbours[rnd.Next(neighbours.Count)];
                    knockWall(c, neighbour);
                    queue.Enqueue(c);
                    c = Cells[neighbour.X, neighbour.Y];
                }
                else
                {
                    c = queue.Dequeue();
                }
            }
        }
    }
}
Och detta är resultatet:
 

Lite nyheter i C# 7: Lokala funktioner

Till en viss grad har man kunnat skapa lokala funktioner i C# under en tid.

public void DoSomething()
{
    Func<int, int> d = (x) => x * 2;
    for (var i = 1; i <= 8; i++)
        Console.WriteLine(d(i));
}

I C# 7 kan man skapa fullvärdiga funktioner i en annan funktion. Det enda som egentligen skiljer en lokal funktion från en medlemsfunktion (eller en static) är att lokala funktioner inte har någon angiven synlighet, eftersom det inte är relevant.

public void DoSomething()
{
    int d(int x) => x * 2;
    for (var i = 1; i <= 8; i++)
        Console.WriteLine(d(i));
}

Fördelen med riktiga lokala funktioner är att det ger tillgång till parameterarrayer, referensparametrar, utparametrar och generics, alltså sådant som inte kunde användas i gamla inline-funktioner.

Closure-hanteringen är lite intressant. Följande exempel använder bara gammal C# och kompilerar inte, eftersom variabeln MinFunktion som håller inlinefunktionen inte finns än.

public void DoSomething()
{
    MinFunktion();
    int x = 10;
    Action MinFunktion = () => x++;
    Console.WriteLine(x);
}

Om vi istället flyttar ner anropet, skrivs 11 ut på skärmen, eftersom x ökas med 1 i MinFunktion.

public void DoSomething()
{
    int x = 10;
    Action MinFunktion = () => x++;
    MinFunktion();
    Console.WriteLine(x);
}

Resultatet blir detsamma om jag ersätter MinFunktion med en fullvärdig lokal funktion. Det x som ökas i MinFunktion är en closure.

public void DoSomething()
{
    int x = 10;
    void MinFunktion() => x++;
    MinFunktion();
    Console.WriteLine(x);
}

Men detta är riktigt intressant. Eftersom MinFunktion nu är en fullvärdig funktion, så skapas den i den yttre funktionen DoSomething vid kompilering. Så nu kan jag anropa den innan variabeln x finns.

public void DoSomething()
{
    MinFunktion();
    int x = 10;
    void MinFunktion() => x++;
    Console.WriteLine(x);
}

Svaret blir då 10, eftersom det inte är closure-variabeln x som blir ökad, utan en x som är lokal för MinFunktion.

Slutligen, eftersom lokala funktioner är fullvärdiga funktioner, kan de innehålla egna lokala funktioner. Snyggt!

Så vad fungerar inte?

Generellt sett kan en värdetyp inte innehålla null, men en referenstyp kan innehålla null. Detta är vettigt, eftersom en referenstyp innehåller just en referens som kan vara eller inte vara satt.

Vi har haft nullable types i C# en tid nu. Det är en struktur som ger värdetyper en null-flagga, så att dessa kan vara null.

public int X { get; set; } //Ej initierad - innehåller 0.
public int? Y { get; set; } //Ej initierad - innehåller null.

Snart ska man kunna förhindra referenstyper från att vara null, vilket är praktiskt t.ex. om man vill att en parameter ska vara obligatorisk. Troligen kommer det att se ut ungefär så här:

public MyType A { get; set; } //Kan innehålla null
public MyType! B { get; set; } = new MyType(); //Kan inte innehålla null

C# 7 ska ha visst stöd för pattern matching, men det fungerar inte (än).

Inte heller record types fungerar. Record types är klasser som endast innehåller properties, som deklareras med en enda rad kod.

Det finns alltså anledning att titta med på C# 7 längre fram.

Lite nyheter i C# 7: Tuples

Om jag vill skapa en funktion som skickar tillbaka två värden, kan det lösas med en tuple. I .NET Framework är tuples implementerat som en generisk klass. Denna funktion skickar tillbaka en int och en string:

public static Tuple<int, string> DoSomething()
{
    return Tuple.Create(10, "Hello");
}

Ett anrop som tar emot svaren skulle kunna se ut så här. I detta fall är response av typen Tuple<int, string>, vilket innebär att Item1 är en int och Item2 en string.

var response = DoSomething();
Console.WriteLine(response.Item1); //10
Console.WriteLine(response.Item2); //"Hello"

Eftersom C# 7 har inbyggt stöd för tuples, behöver jag inte hänvisa till klassen Tuple. Det går helt enkelt att deklarera fler returer på en funktion.

Samma funktion i C# 7 kan se ut så här (givet att NuGet-paketet System.ValueTuple är på plats):

public static (int, string) DoSomething()
{
    return (10, "Hello");
}

Koden som utför anropet kan se lika dan ut. Variabeln response är nu en (int, string) istället för en Tuple<int, string>.

Funktionens returvärden kan dessutom ha specificerade namn, vilket gör koden mer självdokumenterande.

public static (int i, string s) DoSomething()
{
    return (10, "Hello");
}

Anropet:

var response = DoSomething();
Console.WriteLine(response.i); //10
Console.WriteLine(response.s); //"Hello"

Tuples kan skapas inline (x nedan), inline med namngivna medlemmar (y nedan) eller inline med type inference (z nedan).

(int, int, int) x = (10, 20, 30);
Console.WriteLine(x.Item1); //10

(int kalle, int pelle, int sven) y = (10, 20, 30);
Console.WriteLine(y.kalle); //10

var z = (10, 20, 30);
Console.WriteLine(z.Item1); //10

List invocations in constructor

This example shows how to list the method invocations in a C# file. This is the file that will be read:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
  public class MyClass
  {
    public MyClass()
    {
      Console.WriteLine("Hello.");
      DoSomething();
      DoSomethingElse();
    }

    private void DoSomething()
    {
    }

    private void DoSomethingElse()
    {
    }
  }
}

To install CodeSearchTree in your Visual Studio project, type Install-Package CodeSearchTree in the package manager console. Your project must use .NET Framework 4.6 or higher.

The path to the first method invocation is ns/cls/constructor/block/expression/invocation, but since each invocation is located under an expression node, we will iterate through those. The code will list the three calls made in the constructor of the example class above.

//Load the C# class file.
var tree = CodeSearchTree.Node.CreateTreeFromFile(@"MyClass.cs");

//Get the first expression in the constructor.
var exp = tree.GetChild("ns/cls/constructor/block/expression");

while (exp != null)
{
  //Get the invocation and type out the source code.
  var invocation = exp.GetChild("invocation");
  if (invocation != null)
    Console.WriteLine(invocation.Source);

  //Get the next node. Note that the next node might be of any kind.
  exp = exp.GetNextSibling();
}

If you like to use the typed version of the CodeSearchTree API, your code should look like this:

//Load the C# class file.
var tree = CodeSearchTree.Node.CreateTreeFromFile(@"MyClass.cs");

//Get the first expression in the constructor.
var exp = tree.Ns.Cls.Constructor.Block?.Expression?.SearchResult;

while (exp != null)
{
  //Get the invocation and type out the source code.
  var invocation = exp.Invocation?.SearchResult;
  if (invocation != null)
    Console.WriteLine(invocation.Source);

  //Get the next node. Note that the next node might be of any kind.
  exp = exp.GetNextSibling();
}

The output in both cases will be the source code for the three invocations. But let’s say that you want the function names only. The names will be found in different places for different invocations. The first invocation a memberaccess node and two id nodes. The first is called Console and the second is the name of the function. The two other invocations consists of only one id node. We must look at two places. This code will list the three function names only.

//Load the C# class file.
var tree = CodeSearchTree.Node.CreateTreeFromFile(@"MyClass.cs");

//Get the first expression in the constructor.
var exp = tree.GetChild("ns/cls/constructor/block/expression");

while (exp != null)
{
  //Get the invocation and type out the source code.
  var invocation = exp.GetChild("invocation");
  if (invocation != null)
  {
    var id = invocation.GetChild("memberaccess/id[1]");
    if (id != null)
      Console.WriteLine(id.Name);
    else
    {
      id = invocation.GetChild("id");
      if (id != null)
        Console.WriteLine(id.Name);
    }
  }
  //Get the next node. Note that the next node might be of any kind.
  exp = exp.GetNextSibling();
}

Again, if you like to use the typed version of the CodeSearchTree API, your code should look like this:

//Load the C# class file.
var tree = CodeSearchTree.Node.CreateTreeFromFile(@"MyClass.cs");

//Get the first expression in the constructor.
var exp = tree.Ns.Cls.Constructor.Block?.Expression?.SearchResult;

while (exp != null)
{
  //Get the invocation and type out the source code.
  var invocation = exp.Invocation?.SearchResult;
  if (invocation != null)
  {
    //No need for SearchResult property since the indexer gives the search result.
    var id = invocation?.MemberAccess?.Id[1];
    if (id != null)
      Console.WriteLine(id.Name);
    else
    {
      id = invocation?.Id?.SearchResult;
      if (id != null)
        Console.WriteLine(id.Name);
    }
  }

  //Get the next node. Note that the next node might be of any kind.
  exp = exp.GetNextSibling();
}

Output:

WriteLine
DoSomething
DoSomethingElse

Project website: https://github.com/Anders-H/CodeSearchTree

Parse Transact SQL

To parse T-SQL from C#, add a reference to the following libraries (listed under Extensions, not under Framework):

Microsoft.Data.Schema.ScriptDom
Microsoft.Data.Schema.ScriptDom.Sql

My examples imports the following namespaces:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Data.Schema.ScriptDom;
using Microsoft.Data.Schema.ScriptDom.Sql;
using System.IO;

This code shows how to parse a boolean expression:

//Parse
var p = new TSql100Parser(false);
IList<ParseError> err;
var sql = "(a > 10) and (i < 10)";
var exp = p.ParseBooleanExpression(new StringReader(sql), out err);

//Errors? Empty if none.
Console.WriteLine($"{err.Count} errors.");
err.ToList().ForEach(x =>
   Console.WriteLine(
      $"Error \"{x.Message}\" at line {x.Line}."));

//Result. Null if errors.
if (!(exp == null))
   exp.ScriptTokenStream.ToList().ForEach(x =>
      Console.WriteLine(
         $"Token type: {x.TokenType}, source: {x.Text}"));

If you want to parse a complete Transact SQL program, call the ParseStatementList function instead of the ParseBooleanExpression function.

//Parse
var p = new TSql100Parser(false);
IList<ParseError> err;
var sql = "SELECT EmployeeID, FirstName FROM dbo.Employees WHERE FirstName LIKE 'N%'";
var exp = p.ParseStatementList(new StringReader(sql), out err);

Console.WriteLine($"{err.Count} errors.");
err.ToList().ForEach(x =>
   Console.WriteLine(
      $"Error \"{x.Message}\" at line {x.Line}."));

//Result. Null if errors.
if (!(exp == null))
   exp.ScriptTokenStream.ToList().ForEach(x =>
      Console.WriteLine(
         $"Token type: {x.TokenType}, source: {x.Text}"));

This is the expected output:

sqlparse

CodeSearchTree use case: Read constant values

For this example, I am using Visual Studio 2015 Community edition to create a console application.  Make sure that you are using .NET Framework 4.6 or later. Let’s say that you have a .cs file with constants that looks something like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MySystem
{
  public class Settings
  {
    public const int Iterations = 56;
    public const string TargetFilename = @"C:\Output\log.txt";
    public const int Length = 128;
    public const bool Repeat = true;
  }
}

You want to know the names and values in this file. Let’s start from the beginning. In my console application, I want a reference to the CodeSearchTree – I accept your complements for my choice of name. Just to be on the safe side, I specify the name of the project that use the CodeSearchTree library from when I add the package in the Package Manager Console.

Get-Project MyProject | Install-Package CodeSearchTree

In the Main function, first declare the filename variable, then:

//Load and parse the source file.
var tree = CodeSearchTree.Node.CreateTreeFromFile(filename);
//Get the first constant.
var f = tree.GetChild("ns/cls/field");
//Display name and value and get the next constant.
while (!(f == null))
{
  if (f.NodeType == CodeSearchTree.NodeType.FieldDeclarationSyntaxNode)
  {
    //Display...
    var n = f.Name;
    var x = "vardeclaration/vardeclarator/equalsvalue/literal";
    var v = f.GetChild(x).Source;
    Console.WriteLine($"Name: {f.Name}, Value: {v}");
  }
  //Get next...
  f = f.GetNextSibling();
}

If you are unsure about the path strings in your C# tree, try to drop your file on the test client. Happy searching!

Snittvärde

Jag fick en fråga om att räkna ut snittvärden. Det finns olika sätt att lösa det på, där det enklaste kanske är att använda Excel för att göra ett enkelt datablad. Jag tänkte visa hur man kan göra i C# för att få en start till ett registerprogram. För detta använder jag Visual Studio 2015 Community Edition som kan laddas hem gratis här. Välj att skapa ett nytt projekt.

bild1

Jag kör mot .NET Framework 4.5.2 (1) och har valt kategorin Windows under C# (2) och projekttypen Console Application (3). Se till att välja ett lämpligt projektnamn och en lämplig plats att spara projektet på (4). Sen är det bara att börja skriva.

Här har vi den grundläggande principen:

         //Några flyttal att beräkna snitt på.
         double[] tal = { 3.0, 4.0, 1.5, 2.0 };

         //Summera talen.
         var summa = 0.0;
         for (int i = 0; i < tal.Length; i++)
            summa += tal[i];

         //Dividera med antalet.
         var snitt = summa / tal.Length;

         //Presentera resultatet.
         Console.WriteLine(snitt);

         //Pausa tills användaren trycker Enter.
         Console.ReadLine();

Vi borde nu se att snittet av 3.0, 4.0, 1.5 och 2.0 är 2.625.

Eftersom vi har tillgång till namnrymden System.Linq så kan vi använda en inbyggd funktion för att räkna ut snittet. Detta ger samma resultat:

         //Några flyttal att beräkna snitt på.
         double[] tal = { 3.0, 4.0, 1.5, 2.0 };

         //Be om snittet.
         var snitt = tal.Average();

         //Presentera resultatet.
         Console.WriteLine(snitt);

         //Pausa tills användaren trycker Enter.
         Console.ReadLine();

Detta kan vi applicera om vi bygger en struktur som innehåller ett flyttal. Följande kod beskriver ett musikalbum med låtar. Låtarna har ett betyg mellan 1 och 10, och deras snitt ger oss ett betyg på musikalbumet.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Snitt
{
   class Program
   {
      static void Main(string[] args)
      {
         //Konstruera ett album.
         var a = new Album() { AlbumName = "A Night at the Opera",
                               ArtistName = "Queen" };
         //Lägg på låtar.
         a.Songs.Add(new Song() { TrackNumber = 1,
                                  TrackName = "Death on Two Legs",
                                  Rating = 7.0 });
         a.Songs.Add(new Song() { TrackNumber = 2,
                                  TrackName = "Lazing on a Sunday Afternoon",
                                  Rating = 4.0 });
         a.Songs.Add(new Song() { TrackNumber = 3,
                                  TrackName = "I'm in Love with My Car",
                                  Rating = 6.0 });

         //Hämta låtarnas betyg.
         var ratings = (from s in a.Songs
                       select s.Rating).ToArray();

         //Hämta ut låtarnas snittvärde och avrunda.
         var snitt = Math.Round(ratings.Average(), 2);

         //Presentera resultatet.
         Console.WriteLine(snitt);

         //Pausa tills användaren trycker Enter.
         Console.ReadLine();
      }
   }

   //Definiera en låt.
   public class Song
   {
      public int TrackNumber { get; set; }
      public string TrackName { get; set; }
      public double Rating { get; set; }
   }

   //Definiera ett musikalbum
   public class Album
   {
      public string ArtistName { get; set; }
      public string AlbumName { get; set; }
      public List<Song> Songs { get; set; } = new List<Song>();
   }
}

Detta ger oss svaret 5.67.

Klasserna Song och Album kan användas som en del av en Windows-applikation med ett grafiskt användargränssnitt. Projekttypen skulle kunna vara en Windows Forms Application. Att sammanställa en sådan applikation är ett ganska stort och tidskrävande projekt. En album-lista ska skapas, serialisering och deserialisering ska implementeras och det grafiska användargränssnittet ska byggas.

Anropa alla klienter från ASP.NET

SignalR är ett ramverk som låter webbklienter prata direkt med webbservern via sockets, och som låter webbservern prata direkt med klienterna på samma sätt. Tack vare SignalR kan du göra funktionsanrop direkt till de användare som för tillfället råkar surfa på din hemsida. SignalR bygger ovanpå ramverket OWIN för ASP.NET.

I detta exempel arbetar jag i Visual Studio 2013 med ett projekt av typen ASP.NET MVC, så jag utgår alltså ifrån Microsofts exempelprogram som levereras som mall i Visual Studio 2013.

signalr1

För att komma igång ska vi:

  • Installera paketet SignalR i projektet.
  • Läsa in JavaScript-filen för SignalR.
  • Koppla SignalR-hubbarna till OWIN.
  • Skapa en hubb.

Därefter är det fritt fram att börja kommunicera!

För att installera SignalR, skriv följade i Package Manager Console:

Install-Package Microsoft.AspNet.SignalR

Eftersom SignalR ligger i version 2.2 när jag skriver detta, så kompletteras mitt program med filerna jquery.signalR-2.2.0.js och jquery.signalR-2.2.0.min.js i mappen Scripts, men tänk på att versionsnumret kan variera om du vill testa detta.

Därefter ska det minimerade scriptet inkluderas i de vyer som SignalR ska fungera på. Jag hade tänkt mig att kunna använda den överallt, så jag lägger in scriptet i Views/Shared/_Layout.cshtml. Vi ska även läsa in det autogenererade scriptet signalr/hubs.

<script type="text/javascript"
   src="~/scripts/jquery.signalR-2.2.0.min.js"></script>

Jag föredrar att flytta upp de script som redan ligger på sidan till dokumentets head-sektion, så att den ser ut så här:

<head>
   <meta charset="utf-8" />
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>@ViewBag.Title - My ASP.NET Application</title>
   @Styles.Render("~/Content/css")
   @Scripts.Render("~/bundles/modernizr")
   @Scripts.Render("~/bundles/jquery")
   @Scripts.Render("~/bundles/bootstrap")
   <script type="text/javascript"
     src="~/scripts/jquery.signalR-2.2.0.min.js"></script>
   
   @RenderSection("scripts", required: false)
</head>

I nästa steg ska vi koppla SignalR-hubbarna till OWIN. Hubbarna, alltså kommunikationsnaven, har ett client-attribut som används för att prata med klienten och ett server-attribut som används för att prata med servern, och identifieras av adressen “/signalr” om vi inte sagt något annat. För att göra kopplingen behöver vi ha en OWIN Startup class. Högerklicka på ditt projektnamn i Solution Explorer, välj att lägga till en OWIN Startup class. Kalla den för OwinStartup. Det ger oss följande kod:

using System;
using System.Threading.Tasks;
using Microsoft.Owin;
using Owin;

[assembly: OwinStartup(typeof(WebApplication3.OwinStartup))]

namespace WebApplication3
{
   public class Startup
   {
      public void Configuration(IAppBuilder app)
      {
         // For more information on...
      }
   }
}

Ersätt kommentaren i funktionen Configuration med följande anrop:

app.MapSignalR();

Nu har vi gjort de förberedelser som behövs för att kommunicera. Till att börja med tänkte jag att klienten ska få skicka ett meddelande till servern när användaren klickar på en knapp.

Den sista förberedelsen innan vi kan börja är att skapa en hubb. En hubb är en klass som ärver från Microsoft.AspNet.SignalR.Hub. Kontrollera att du har mappen App_Code, annars kan den skapas från menyn som visas när man högerklickar på projektnamnet. Sedan, högerklicka på mappen App_Code och lägg till en klass av typen SignalR Hub Class (v2). Ge den namnet MinTestHub. Det går också att lägga till en vanlig klass och läsa in namnrymden Microsoft.AspNet.SignalR, och sedan ärva från Hub. Här i kan vi skriva de funktioner som vi vill exponera för klienterna, i mitt fall SkickaMeddelande.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.AspNet.SignalR;

namespace WebApplication3.App_Code
{
   public class MinTestHub : Hub
   {
      public void SkickaMeddelande(string meddelande)
      {
         this.Clients.All.servernPratar(
            "Ett meddelande: " + meddelande);
      }
   }
}

Visual Studios texteditor är inte speciellt hjälpsam eftersom koden är dynamisk och okänd för Visual Studio, men detta betyder att när en klient får för sig att anropa servern med SignalR, så svarar servern genom att anropa alla klienter.

Tänk på att hubben exponeras som ett attribut till ett JavaScript-objekt, connection, på klientsidan med första bokstaven som gemen, vilket betyder att denna hubb kommer att heta minTestHub på klienten – inte MinTestHub.

För att summera klassen MinTestHub: Funktionen SkickaMeddelande exponeras för klienterna och låter dem prata med servern. Servern svarar genom att anropa JavaScript-funktionen servernPratar hos alla klienterna.

Det som återstår nu är klientkoden. Jag skriver den i applikationens startsida, Views/Home/Index.cshtml.

På en lämplig plats vill jag ha en knapp kallad SendFromClient och därefter öppnar jag ett script-block.

<input type="button" id="SendFromClient" value="Send from client" />
<script type="text/javascript">

</script>

All kod skriver jag i jQuery:s uppstartevent. Det innebär att hela koden är inkapslad av $(function () { och });. I script-blocket gör jag två saker. Först ansluter jag mig till hubben och tillhandahåller definitionen av servernPratar, alltså skriver vad som ska hända när ett anrop från servern kommer in.

var testhub = $.connection.minTestHub;
testhub.client.servernPratar = function (meddelande) {
   alert(meddelande);
};

Därefter, när hubben är startad, så talar jag om vad som ska hända när knappen blir klickad på. Då vill jag anropa funktionen skickaMeddelande på servern.

$.connection.hub.start().done(function () {
   $("#SendFromClient").click(function () {
      testhub.server.skickaMeddelande("Hej från klienten!");
   });
});

Hela klientkoden som jag infogar ser alltså ut så här:

<input type="button" id="SendFromClient" value="Send from client" />

<script type="text/javascript">
   $(function () {

      var testhub = $.connection.minTestHub;
      testhub.client.servernPratar = function (meddelande) {
         alert(meddelande);
      };

      $.connection.hub.start().done(function () {
         $("#SendFromClient").click(function () {
            testhub.server.skickaMeddelande("Hej från klienten!");
         });
      });
   });
</script>

Om du testar att öppna flera webbläsare och surfar till din applikation, kommer du att se att när du klickar på knappen på en av dem, så visar sig meddelanderutan i alla. (För bäst effekt, använd olika webbläsare med tanke på att en meddelanderuta kan blockera andra meddelanderutor.)

Hubben i din server blir hårt belastad kan meddelanden skickas i okontrollerad ordning. Det beror på att anropen till klienten är asynkrona, men detta kan avhjälpas med async och await.

Om aktörer

Det finns tre olika typer av aktörer i Akka .NET. Nedan använder jag en ReceiveActor som utmärker sig genom att den använder funktionen Receive för att ta emot ett meddelande. Exemplen som följer förutsätter att namnrymden Akka.Actor är inläst. Alla aktörer har en sökväg som är oberoende av vilken CLR-klass som beskriver dem. Givet att system är ett initierat aktörssystem med namnet MittSystem och att vi har två aktörer…

public class EnRootActor : ReceiveActor
{
}

public class EnAnnanActor : ReceiveActor
{
}

…så kommer denna kod att fallera eftersom båda aktörerna, trots sina olika klasser, gör anspråk på samma sökväg:

var a = system.ActorOf<EnRootActor>("MinRoot");
Console.WriteLine(a.Path);
var b = system.ActorOf<EnAnnanActor>("MinRoot");
Console.WriteLine(b.Path);

Jag använder Akka .NET lokalt, vilket syns på sökvägen som är akka://MittSystem/user/MinRoot vilket innebär protokollet akka, aktörssystemet MittSystem följt av sökvägen user/MinRoot. Alla användardefinierade aktörer ligger under user.

Både a och b kan samexistera givet att vi ger dem unika namn.

Aktörer behöver inte skapas på rootnivå. Om en aktör skapar en aktör organiseras den nya aktören under den som skapar aktören.

public class EnRootActor : ReceiveActor
{
   public EnRootActor()
   {
      this.Receive<Message>(m =>
      {
         var c = Context.ActorOf<EnAnnanActor>("Child");
         Console.WriteLine(c.Path);
      });
   }
}

I ovanstående fall har c sökvägen akka://MittSystem/user/MinRoot/MinChild, och eftersom vi har ett villkorslöst skapande så stöter vi på problem nästa gång vi skickar ett meddelande till EnRootActor eftersom MinChild inte kan skapas mer än en gång.

Akka .NET erbjuder möjligheten att erhålla referenser till existerande aktörer genom funktionen ActorSelection som kan agera antingen på systemet eller i kontexten för någon existerande aktör.

I detta exempel (en Console Application) är skapas en tredje aktör när aktör a hanterar meddelandet som skickas till den. Den tredje aktören får sedan ett meddelande skickat till sig från Main, vilket är en annan tråd.

class Program
{
   private static ActorSystem system;

   static void Main(string[] args)
   {
      var system = ActorSystem.Create("MittSystem");
      var a = system.ActorOf<EnRootActor>("MinRoot");
      Console.WriteLine(a.Path);
      var b = system.ActorOf<EnAnnanActor>("EnTillRoot");
      Console.WriteLine(b.Path);
      a.Tell(new Message("Helloooooo!"));

      //Godtycklig paus eftersom childaktören
      //skapas på en annan tråd.
      System.Threading.Thread.Sleep(1000);

      //Erhåll en referens till childaktören
      //och skicka ett meddelande.
      system.ActorSelection(
         "akka://MittSystem/user/MinRoot/Child").Tell(
         new Message("Snodd ref."));

      Console.ReadLine();
   }
}

public class EnRootActor : ReceiveActor
{
   public EnRootActor()
   {
      this.Receive<Message>(m =>
      {
         Console.WriteLine(m.Text);
         //Skapa en ny.
         var c = Context.ActorOf<EnAnnanActor>("Child");
         Console.WriteLine(c.Path);
      });
   }
}
   
public class EnAnnanActor : ReceiveActor
{
   public EnAnnanActor()
   {
      this.Receive<Message>(m => Console.WriteLine(m.Text));
   }
}

public class Message
{
   public string Text { get; private set; }
   public Message(string text)
   {
      this.Text = text;
   }
}

För att inte programmet ska krascha nästa gång EnRootActor tar emot meddelandet, kan man kort och gott kolla om childaktören redan är skapad.

public class EnRootActor : ReceiveActor
{
   public EnRootActor()
   {
      this.Receive<Message>(m =>
      {
         Console.WriteLine(m.Text);

         //Skapa en ny om det behövs.
         if (Context.GetChildren().Count() <= 0)
         {
            var c = Context.ActorOf<EnAnnanActor>("Child");
            Console.WriteLine(c.Path);
         }
      });
   }
}

Skulle man till äventyrs vara intresserad av vilka aktörer som är childaktörer går det bra att läsa av deras sökväg eller kanske bara deras namn:

foreach (var x in Context.GetChildren())
   Console.WriteLine(x.Path.Name);

Så här ser samma exempel ut i Visual Basic (förutsatt att du lagt på Akka .NET med NuGet):

Imports Akka.Actor

Module Module1

   Private sys As ActorSystem

   Sub Main()
      sys = ActorSystem.Create("MittSystem")
      Dim a = sys.ActorOf(Of EnRootActor)("MinRoot")
      Console.WriteLine(a.Path)
      Dim B = sys.ActorOf(Of EnAnnanActor)("EnTillRoot")
      a.Tell(New Message("Helloooooo!"))
      System.Threading.Thread.Sleep(1000)
      sys.ActorSelection( _
         "akka://MittSystem/user/MinRoot/Child").Tell( _
         New Message("Snodd ref."))
      a.Tell(New Message("Tjo!"))
      Console.ReadLine()
   End Sub

End Module

Public Class EnRootActor
   Inherits ReceiveActor

   Public Sub New()
      Me.Receive(Of Message)(AddressOf Me.ReceiveMessage)
   End Sub

   Private Sub ReceiveMessage(m As Message)
      Console.WriteLine(m.Text)
      If Context.GetChildren().Count() <= 0 Then
         Dim c = Context.ActorOf(Of EnAnnanActor)("Child")
         Console.WriteLine(c.Path)
      End If
   End Sub

End Class

Public Class EnAnnanActor
   Inherits ReceiveActor

   Public Sub New()
      Me.Receive(Of Message)(AddressOf Me.ReceiveMessage)
   End Sub

   Private Sub ReceiveMessage(m As Message)
      Console.WriteLine(m.Text)
   End Sub

End Class

Public Class Message

   Public Property Text() As String

   Public Sub New(Text As String)
      Me.Text = Text
   End Sub

End Class

Förutom ReceiveActor som alltså anropar funktionen Receive för att ta emot meddelanden, så har vi även TypedActor och UntypedActor.

En TypedActor anropar inte funktionen Receive för att ta emot meddelanden, utan har en överlagring av Handle för varje meddelandetyp som aktören kan hantera. En TypedActor ärver därför inte bara från basklassen TypedActor, utan även från det generiska interfacet IHandle<> för varje meddelandetyp som aktören ska hantera. Om man jobbar med klassen TypedActor betalar man med lite minskad flexibilitet, men vinner i ökad struktur. I detta exempel kan aktören ta emot både meddelande av typen A och B (där A och B är vanliga CLR-klasser).

class Program
{
   private static ActorSystem system;

   static void Main(string[] args)
   {
      var system = ActorSystem.Create("MittSystem");
      var actor = system.ActorOf<MyTypedActor>("MinActor");
      actor.Tell(new A());
      actor.Tell(new B());
      Console.ReadLine();
   }
}

public class MyTypedActor : TypedActor, IHandle<A>, IHandle<B>
{
   public void Handle(A message)
   {
      Console.WriteLine("Tack för A!");
   }

   public void Handle(B message)
   {
      Console.WriteLine("Tack för B!");
   }
}

public class A
{
}

public class B
{
}

En UntypedActor fungerar enligt samma princip, fast utan typer. Den har en enda funktion som blir anropad vid inkommande meddelande, och den funktionen tar emot meddelandet i object-format. Jag personligen kan inte se någon speciell poäng i att välja att ärva från UntypedActor, för om man inte har behov av typlåsningen så är ReceiveActor ett bättre val. Notera att UntypedActor skriver över funktionen OnReceive.

class Program
{
   private static ActorSystem system;

   static void Main(string[] args)
   {
      var system = ActorSystem.Create("MittSystem");
      var actor = system.ActorOf("MinActor");
      actor.Tell(new A());
      actor.Tell(new B());
      Console.ReadLine();
   }
}

public class MyUntypedActor : UntypedActor
{
   protected override void OnReceive(object message)
   {
      if (message.GetType() == typeof(A))
         Console.WriteLine("Tack för A!");
      else if (message.GetType() == typeof(B))
         Console.WriteLine("Tack för B!");
   }
}

public class A
{
}

public class B
{
}

Schemalagda meddelanden i Akka .NET

Denna gång tänker jag använda en Windows Forms Application för att visa schemaläggaren i Akka .NET. Som vanligt, för att komma åt Akka .NET i ditt projekt, växla till fönstret Package Manager Console och skriv Install-Package Akka. Om du inte ser Package Manager Console, klicka på rullgardinsmenyn Tools, välj Library Package Manager och Package Manager Console.

Programmet består av följande meddelandeklass:

public class TimedMessage
{
   public string Message { get; private set; }
   public TimedMessage(string message)
   {
      this.Message = message;
   }
}

…och följande aktör:

public class Listen : ReceiveActor
{
   public Listen()
   {
      this.Receive<TimedMessage>(x =>
      {
         System.Diagnostics.Debug.WriteLine(x.Message);
      });
   }
}

Huvudfönstret har läst in namnrymden Akka.Actor för att några extension methods ska vara tillgängliga…

using Akka.Actor;

…och innehåller en knapp som heter button1. I formulärets Load-event skapas aktörssystemet och aktören och under knappen så skickar jag ett meddelande (TimedMessage) till aktören.

public partial class Form1 : Form
{
   private ActorSystem actorSystem;
   private IActorRef listen;

   public Form1()
   {
      InitializeComponent();
   }

   private void button1_Click(object sender, EventArgs e)
   {
      this.listen.Tell(new TimedMessage("Helloooo!"));
   }

   private void Form1_Load(object sender, EventArgs e)
   {
      this.actorSystem = ActorSystem.Create("ActorSystem");
      this.listen = this.actorSystem.ActorOf(
         Props.Create(typeof(Listen)));
   }
}

Om du testar detta lilla exempel så märker du att meddelandet går iväg till aktören i samma ögonblick som du klickar på knappen. Men istället för att anropa aktörens Tell-funktion i Click-eventet, så kan vi använda aktörssystemets Scheduler-property. Den har ett antal funktioner för schemaläggning och upprepning. Följande förändring gör att meddelandet går iväg först tre sekunder efter att man klickar på knappen.

private void button1_Click(object sender, EventArgs e)
{
   this.actorSystem.Scheduler.ScheduleTellOnce(
      TimeSpan.FromSeconds(3), this.listen,
      new TimedMessage("Helloooo!"), ActorRefs.Nobody);
}

Funktionen ScheduleTellOnce schemalägger ett meddelande istället för att skicka iväg det direkt. Det är det första argumentet som bestämmer när meddelandet ska gå iväg. Uttrycket TimeSpan.FromSeconds(3) säger att meddelandet ska gå iväg efter tre sekunder, oavsett vad klockan är. Schemaläggaren förväntar sig att man anger relativa tider, så om jag vill att meddelandet ska gå iväg ett bestämt klockslag, t.ex. den 19/5 kl. 13:30, kan jag istället skriva new DateTime(2015, 5, 19, 13, 30, 0).Subtract(DateTime.Now).

Det andra argumentet är aktören som ta emot meddelandet. I mitt exempel finns endast en aktör, så den skickar jag in. Det tredje argumentet är meddelandet som ska skickas. Slutligen, det fjärde argumentet är avsändaren. Om det är en aktör som skickar meddelandet, skriver man normalt this.Self, men det går bra att skicka in en annan aktör här.

I mitt fall när det inte finns någon aktör som avsändare (det är ju formuläret som skickar) så kan det vara frestande att skicka in null, men i dagsläget fungerar inte det. Klassen ActorRefs innehåller konstanter som kan användas som substitut för en avsändare, och Nobody betyder just ingen. (Detta kan komma att ändras.)

Men tänk dig att du vill bli informerad när något inte händer. Från första klicket vill du veta om användaren fortsätter att klicka var tredje sekund, eller om användaren har blivit passiv. Det kan vi lösa på följande vis:

1. När första klicket inträffar, säger vi att vi vill ha ett meddelande om tre sekunder.

2. När knappen blir klickad på nästa gång, inom tre sekunder, så avbryter vi beställningen, och gör om den på nytt.

3. Skulle användaren inte klicka inom tre sekunder så går meddelandet iväg eftersom det inte hunnit avbrytas. Således vet vi att användaren har blivit passiv.

Detta kräver minimala förändringar. Först vill vi ha ett kvitto på beställningen. För detta använder jag en medlemsvariabel av typen Akka.Actor.ICancelable.

private ICancelable idle_detector = null;

Funktionen ScheduleTellOnce ger ingen retur, men ScheduleTellOnceCancelable ger ett svar i ICancelable-format som jag kan lagra i min nya medlemsvariabel. När användaren klickar, kan jag kolla om det finns en liggande beställning, och i så fall kan jag avbryta den – eftersom användaren klickat så är han ju inte inaktiv.

private void button1_Click(object sender, EventArgs e)
{
   if (!(this.idle_detector == null))
      this.idle_detector.Cancel();
   this.idle_detector =
      this.actorSystem.Scheduler.ScheduleTellOnceCancelable(
      TimeSpan.FromSeconds(3), this.listen,
      new TimedMessage("Idle!!!"), ActorRefs.Nobody);
}

För att testa programmet, starta och klicka lite då och då på knappen. För varje gång tre sekunder får passera utan något klick så går meddelandet iväg till aktören.

Akka .NET Remote duplex

Jag har tidigare visat hur man skickar enkla meddelanden med Akka .NET Remote. Exemplet bestod av tre projekt, en server, ett datalager och en klient. Här tänkte jag visa hur man använder Akka .NET Remote för att både skicka och ta emot meddelanden. I detta exempel återanvänder jag servern BioServer och datalagret BioData från det förra exemplet.

vs

Tanken är alltså att programmet är ett enkelt bokningssystem för en liten biograf med tre sittplatser. Denna gång tänker jag utöka datalagret med lite nya meddelanden och servern med kapaciteten att svara på inkommande anrop. Dessutom bygger jag en ny grafisk klient.

Datalagret
I den förra versionen hade vi bara ett enda definierat meddelande i datalagret, klassen Bokningsmeddelande. Detta meddelande finns kvar, men nu kompletterar vi med Bokningssvar som ska skickas tillbaka till klienten som skickar in ett Bokningsmeddelande.

public class Bokningsmeddelande
{
   public int StolNr { get; private set; }
   public int BokadAv { get; private set; }
   public Bokningsmeddelande(int stolNr, int bokadAv)
   {
      this.StolNr = stolNr;
      this.BokadAv = bokadAv;
   }

   public override string ToString()
   {
      return string.Format(
         "Bokning från {0} på stol {1}.",
         this.BokadAv, this.StolNr);
   }
}

public class Bokninssvar
{
   public bool BokningAccepterad { get; private set; }
   public int StolNr { get; private set; }
   public Bokninssvar(bool bokningAccepterad, int stolNr)
   {
      this.BokningAccepterad = bokningAccepterad;
      this.StolNr = stolNr;
   }
}

Servern
Låt oss börja med att implementera stödet för Bokningsmeddelande och Bokningssvar.

Klassen Stollista, alltså den bokningsbara resursen, har jag valt att utöka med en funktion som kan uppge vem som bokat en plats.

public int BokadAv(int stolNr)
{
   var stol = this.Find(m => m.StolNr == stolNr);
   if (stol == null)
      return 0;
   else
      return stol.BokadAv;
}

Det innebär att hela klassen nu ser ut så här:

class Stollista : List
{
   public override string ToString()
   {
      var ret = new System.Text.StringBuilder();
      foreach (var s in this)
         ret.AppendLine(s.ToString());
      return ret.ToString();
   }

   public bool Boka(int stolNr, int bokadAv)
   {
      var stol = this.Find(m => m.StolNr == stolNr);
      if (stol == null)
         return false;
      stol.BokadAv = bokadAv;
      return true;
   }

   public int BokadAv(int stolNr)
   {
      var stol = this.Find(m => m.StolNr == stolNr);
      if (stol == null)
         return 0;
      else
         return stol.BokadAv;
   }
}

Dessutom har jag modifierat aktören Bokningsaktor så att den kontrollerar huruvida den önskade stolen är bokningsbar, och meddelar klienten om bokningen accepteras. Notera hur aktören använder propertyn Sender för att kommunicera med avsändaren.

class Bokningsaktor : Akka.Actor.ReceiveActor
{

   public Bokningsaktor()
   {
      this.Receive(b => {
         Console.WriteLine("Ny bokning!");
         if (Program.stolar.BokadAv(b.StolNr) == 0 ||
            Program.stolar.BokadAv(b.StolNr) == b.BokadAv)
         {
            Program.stolar.Boka(b.StolNr, b.BokadAv);
            this.Sender.Tell(new BioData.Bokninssvar(true, b.StolNr));
         }
         else
            this.Sender.Tell(new BioData.Bokninssvar(false, b.StolNr));
         Console.WriteLine(Program.stolar.ToString());
      });
   }
}

Den nya klienten
I detta fall vill jag ha en grafisk klient för att kunna representera svaren från servern på ett någorlunda snyggt sätt. Jag har skapat en applikation av typen Windows Forms Application, satt referenser till Akka .NET och Akka .NET Remote med hjälp av NuGet samt satt en referens till datalagret BioData. Dessutom vill jag läsa in namnrymden Akka.Actor för att komma åt en extension method – en överlagring av funktionen ActorOf.

using Akka.Actor;

Vidare väljer jag att hålla en referens till mitt aktörssystem i huvudformuläret.

private Akka.Actor.ActorSystem system;

På formuläret har jag placerat ut ett textfält där användaren förväntas infoga sitt användar-ID (ett heltal) samt tre knappar som representerar de tre bokningsbara stolarna. Man förväntas ange sitt ID i textfältet innan man klickar på någon knapp.

form

I formulärets Load-event initierar jag aktörssystemet på samma sätt som tidigare, men sparar nu referensen i medlemsvariabeln som jag deklarerade ovan.

private void Form1_Load(object sender, EventArgs e)
{
   var config = Akka.Configuration.ConfigurationFactory.ParseString(@"
akka {
    actor {
        provider = ""Akka.Remote.RemoteActorRefProvider, Akka.Remote""
    }
    remote {
        helios.tcp {
            port = 0
            hostname = localhost
        }
    }
}
");
   this.system = ActorSystem.Create("Bokningsklient", config);         
}

Eventet FormClosed får sköta uppstädningen.

private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
   this.system.Shutdown();
   this.system.Dispose();
}

Sedan ska Click-eventet under varje knapp inleda bokningsförfarandet.

private void btnStol1_Click(object sender, EventArgs e)
{
   this.Boka(1);
}

private void btnStol2_Click(object sender, EventArgs e)
{
   this.Boka(2);
}

private void btnStol3_Click(object sender, EventArgs e)
{
   this.Boka(3);
}

För att aktören ska kunna komma åt knapparna direkt, har jag även en enkel funktion som tar ett stolsnummer och ger en referens till en knapp.

public Button Stolreferens(int StolNr)
{
   Button[] ret = { this.btnStol1, this.btnStol2, this.btnStol3 };
   return ret[StolNr - 1];
}

Sista funktionen i formuläret är den som utför själva bokningen. Notera att vi skickar med en referens till en lokal aktör som ska kunna ta emot svaret från servern.

private void Boka(int StolNr)
{
   var avsandare = this.system.ActorOf();
   var bokare = this.system.ActorSelection(
      "akka.tcp://AllaBokare@localhost:8080/user/Bokare");
   bokare.Tell(new BioData.Bokningsmeddelande(StolNr,
      int.Parse(txtAnvID.Text)), avsandare);
}

Den lokala aktören ser ut så här. Notera att den innehåller lite fulkod för att erhålla en referens till formuläret i fråga. Koden förmodar att formuläret är ensamt i programmet.

public class LocalActor : Akka.Actor.ReceiveActor
{
   public LocalActor()
   {
      this.Receive(s =>
      {
         var stol = (Application.OpenForms[0]
            as Form1).Stolreferens(s.StolNr);
         stol.BackColor = s.BokningAccepterad ? Color.Green : Color.Red;
      });
   }
}

Varje klient som klickar på en knapp för att utföra en bokning, kommer att få en bekräftelse genom att knappen blir grön. Om stolen inte är bokningsbar blir istället knappen röd.

form2

Introduktion till Akka .NET Remote

Akka .NET Remote låter aktörerna i Akka .NET skicka meddelanden till andra processer genom Windows Sockets. Det innebär att ett system kan distribueras över flera processer oavsett om de ligger på samma fysiska maskin eller på en annan maskin i nätverket.

Arkitekturen i Akka .NET Remote är serverfri – varje peer kan kontakta och kommunicera med vilken annan peer som helst, givet att namn och port är känt. Det innebär att om du bygger en server, kan den utan problem förmedla direktkontakt mellan sina klienter.

Detta exempel visar ett mycket enkelt bokningssystem av en biosalong med tre stolar. Systemet består av tre stycken projekt, en server, ett datalager och en klient. Servern och klienten är Console Applications (C#) och datalagret är ett klassbibliotek.

vs

Servern
Låt oss börja med att titta på servern. I mitt exempel heter den BioServer. All kod som visas i servern förutsätter följande:

1. Akka .NET är installerat. Detta kan göras med följande Nuget-kommando:

Install-Package Akka

2. Akka .NET Remote är installerat.

Install-Package Akka.Remote

3. Följande using-sats:

using Akka.Actor;

Så här ser huvudprogrammet ut:

class Program
{
   public static Stollista stolar;

   static void Main(string[] args)
   {
      var config =
         Akka.Configuration.ConfigurationFactory.ParseString(@"
akka {
    actor {
        provider = ""Akka.Remote.RemoteActorRefProvider, Akka.Remote""
    }

    remote {
        helios.tcp {
            port = 8080
            hostname = localhost
        }
    }
}
");

      stolar = new Stollista()
         { new Stol(1), new Stol(2), new Stol(3) };
      Console.WriteLine(stolar.ToString());

      using (var system =
         Akka.Actor.ActorSystem.Create("AllaBokare", config))
      {
         var bokare = system.ActorOf(new
            Akka.Actor.Props(typeof(Bokningsaktor)),"Bokare");
         Console.ReadLine();
      }
   }
}

Notera att konfigurationen anger vilken port servern ska lyssna på (8080). Den kodraden har jag klippt ut från dokumentationen för Akka .NET Remote och klistrat in här. Portnumret måste vara förutbestämt för att den första klienten ska kunna hitta servern.

Därefter har jag min biosalong bestående av tre stolar. Dessa beskrivs av följande klasser:

class Stollista : List<Stol>
{
   public override string ToString()
   {
      var ret = new System.Text.StringBuilder();
      foreach (var s in this)
         ret.AppendLine(s.ToString());
      return ret.ToString();
   }

   public bool Boka(int stolNr, int bokadAv)
   {
      var stol = this.Find(m => m.StolNr == stolNr);
      if (stol == null)
         return false;
      stol.BokadAv = bokadAv;
      return true;
   }
}

Därefter använder jag Akka.Actor.ActorSystem.Create för att skapa mitt aktörssystem (kallad AllaBokare) på samma sätt som om jag hade tänkt att använda Akka .NET i en och samma process, och i using-blocket för aktörssystemet så skapar jag min aktör (kallad Bokare). Det som återstår i servern är klassen som beskriver aktören. Den ser ut så här:

class Bokningsaktor : Akka.Actor.ReceiveActor
{

   public Bokningsaktor()
   {
      this.Receive(b => {
         Console.WriteLine("Ny bokning!");
         Program.stolar.Boka(b.StolNr, b.BokadAv);
         Console.WriteLine(Program.stolar.ToString());
      });
   }
}

Datalagret
Datalagret har jag valt att lägga i ett annat projekt i samma lösning som servern. Jag har kallat datalagret för BioData. Här finns en klass som beskriver meddelandet som klienten kan skicka till servern.

public class Bokningsmeddelande
{
   public int StolNr { get; private set; }
   public int BokadAv { get; private set; }
   public Bokningsmeddelande(int stolNr, int bokadAv)
   {
      this.StolNr = stolNr;
      this.BokadAv = bokadAv;
   }

   public override string ToString()
   {
      return string.Format("Bokning från {0} på stol {1}.",
         this.BokadAv, this.StolNr);
   }
}

Både servern och klienten ska ha en referens till datalagret.

Klienten
Klienten är mycket enkel. Notera först skillnaden i konfigurationen mellan klienten och servern. Servern har port 8080, vilket klienten måste veta om för att kunna hitta servern. Men när klienten gjort sig tillkänna så vet servern vilken port klienten använder, därför behöver vi inte bestämma någon port åt klienten. Att ange port 0 är att begära dynamisk port.

Därefter skapar vi ett aktörssystem med det godtyckliga namnet Bokningsklient. Från systemet i klienten väljer vi att ansluta oss till servern genom att anropa ActorSelection. Som argument använder jag sökvägen till aktören på servern (som hette Bokare i systemet AllaBokare). Slutligen samlar jag in information från slutanvändaren som skickas till servern med metoden Tell. Så här ser källkoden för klienten ut:

class Program
{
   static void Main(string[] args)
   {
      var config = Akka.Configuration.ConfigurationFactory.ParseString(@"
akka {
    actor {
        provider = ""Akka.Remote.RemoteActorRefProvider, Akka.Remote""
    }
    remote {
        helios.tcp {
            port = 0
            hostname = localhost
        }
    }
}
");

      using (var system =
         Akka.Actor.ActorSystem.Create("Bokningsklient", config))
      {
         var bokare = system.ActorSelection(
            "akka.tcp://AllaBokare@localhost:8080/user/Bokare");
         Console.Write("Vad är ditt användar-ID? Skriv ett heltal. ");
         var userId = int.Parse(Console.ReadLine());
         while(true)
         {
            Console.Write("Vilken stol vill du boka? (1-3) ");
            var stol = Console.ReadLine();
            if (stol == "")
               break;
            bokare.Tell(new
               BioData.Bokningsmeddelande(int.Parse(stol), userId));
         }
         system.Shutdown();
      }
   }
}

Bilden visar servern bakom en klient.

akka_remote

Eftersom portnumret tilldelas till klienten dynamiskt så kan flera klienter startas och boka platser. Och eftersom arkitekturen i Akka .NET Remote är peer-to-peer så kan dessa skicka meddelanden till varandra.

Enkel trådning i .NET med Akka

Historiskt sett har det varit ganska komplicerat att bygga multitrådade applikationer i .NET. Rent tekniskt var det enkelt att sjösätta en tråd med hjälp av klassen System.Threading.Thread, men om mycket skulle göras började koden ganska snabbt likna spaghetti. För att minska ner koden kan man använda multitrådade delegater istället för objekt av typen Thread, men det löste inte problemet med spaghettikod. Nyckelorden asynk och await löste både komplexiteten med hopp och returvärde, men var ganska hårt knutet till modellen TAP.

Om jag väljer att jobba med Akka .NET blir trådningen ännu mer hanterad, fast kring en mycket friare modell än TAP, nämligen Actor Model. (I alla nedanstående exempel jobbar jag i Visual Studio 2013.) Tänk dig att jobbar i Windows Forms och att du vill utföra något som kräver ungefär fem sekunders arbete och sedan visar ett meddelande när arbetet är utfört. Min erfarenhet är att Akka .NET fortfarande kan utsättas för förändringar i sitt publika API, så den exakta kodens utseende kan variera – jag kör Akka 1.0.0. För att visa detta skapar jag ett helt nytt Windows Forms-projekt (C# med .NET Framework 4.5.1) och installerar Akka .NET från Package Manager Console genom att skriva:

Install-Package Akka

Om du inte ser Package Manager Console, Klicka Tools, NuGet Package Manager och Package Manager Console.

Akka .NET använder meddelanden för att kommunicera. Meddelanden är kort och gott godtyckliga objekt som initieras och skickas i samband med att ett asynkront anrop görs. Meddelandet bör vara immutable, alltså meddelandet ska inte kunna modifieras, vilket kan implementeras genom att objektets data inte har några publika setters.

class AkkaMessage
{
   public string MyValue { get; private set; }

   public AkkaMessage(string myValue)
   {
      this.MyValue = myValue;
   }
}

En actor (aktör) är den t.ex. det objekt som kan ta emot ett meddelande och agera på det. Det som utmärker en sådan aktör är basklassen Akka.Actor.ReceiveActor samt konstruktorn som tar emot meddelandet som skickas till den. För att ta emot meddelandet används den generiska metoden Receive vars argument är det som ska göras med meddelandet. Den generiska metodens typparameter är meddelandeklassen. Så i följande kod anropar vi Receive och skickar med kod jobbar i fem sekunder för att sedan visa en meddelanderuta.

class AkkaActor : Akka.Actor.ReceiveActor
{
   public AkkaActor()
   {
      this.Receive(x =>
         {
            System.Threading.Thread.Sleep(5000);
            System.Windows.Forms.MessageBox.Show(x.MyValue);
         }
      );
   }
}

Nu återstår bara initieringen och själva anropet. Följande kod har jag skrivit i mitt programs huvudformulär. Först har jag ett privat fält av typen Akka.Actor.ActorSystem som representerar motorn i Akka – det s.k. actorsystemet. I Load-eventet för formuläret initierar jag systemet med funktionen Create. Create vill ha ett namn på systemet i sin kostruktor. Slutligen, under Click-eventet på en knapp på formuläret så gör jag anropet som ska hanteras av aktören ovan.

Den första raden anropar funktionen ActorOf för att erhålla en referens till en aktör. Den andra raden utför utför anropet med hjälp av funktionen Tell, och låter ett meddelande skickas med som argument. Meddelandet måste vara av typen AkkaMessage, eftersom jag hårdkodade aktören att hantera den typen.

public partial class Form1 : Form
{
   private Akka.Actor.ActorSystem actorSystem;

   public Form1()
   {
      InitializeComponent();
   }

   private void Form1_Load(object sender, EventArgs e)
   {
      this.actorSystem =
         Akka.Actor.ActorSystem.Create("ActorSystem");
   }

   private void button1_Click(object sender, EventArgs e)
   {
      var m = this.actorSystem.ActorOf(new
         Akka.Actor.Props(typeof(AkkaActor)));
      m.Tell(new AkkaMessage("Hello!"), m);
   }
}

När du kör programmet, notera att programmets fönster är svarsbenäget under den tid programmet pausar pausar (anropet på Sleep), vilket säger oss att aktören och formuläret körs på olika trådar.

Om du istället hade velat implementera detta i Visual Basic, skulle ditt meddelande kunna se ut så här:

Public Class AkkaMessage
   Public Property MyValue() As String
   Public Sub New(MyValue As String)
      Me.MyValue = MyValue
   End Sub
End Class

Aktören skulle kunna se ut så här:

Public Class AkkaActor
   Inherits Akka.Actor.ReceiveActor

   Public Sub New()
      Me.Receive(Of AkkaMessage)(AddressOf MyMessageHandler)
   End Sub

   Private Sub MyMessageHandler(X As AkkaMessage)
      System.Threading.Thread.Sleep(5000)
      MessageBox.Show(X.MyValue)
   End Sub

End Class

Och slutligen, formuläret skulle kunna se ut så här:

Public Class Form1

   Private ActorSystem As Akka.Actor.ActorSystem

   Private Sub Form1_Load(sender As Object, _
      e As EventArgs) Handles MyBase.Load
      Me.ActorSystem = Akka.Actor.ActorSystem.Create("ActorSystem")
   End Sub

   Private Sub Button1_Click(sender As Object, _
      e As EventArgs) Handles Button1.Click
      Dim M = Me.ActorSystem.ActorOf( _
         New Akka.Actor.Props(GetType(AkkaActor)))
      M.Tell(New AkkaMessage("Hello!"), M)
   End Sub

End Class

Simple full screen support in Windows Forms

These simple steps lets you create a Windows Forms application with full screen support. I have changed the DoubleBuffered property to true for flicker free redrawing, StartPosition to Manual to be able to manage startup position in code and KeyPreview to true.

This code in the Load event handler will position the window and restore the fullscreen state.

var s = this.GetScreen();
this.Top = s.WorkingArea.Top + 40; this.Left = s.WorkingArea.Left + 40;
this.Width = s.WorkingArea.Width - 80; this.Height =
                                             s.WorkingArea.Height - 80;
if ((Application.UserAppDataRegistry.GetValue("Fullscreen",
                                         "false") as string) == "true")
   this.ToggleFullscreen();

This is the KeyDown event handler that will enable the user to press F11 or Alt+Enter to toggle fullscreen.

private void MainForm_KeyDown(object sender, KeyEventArgs e)
{
   if(((e.KeyCode == Keys.Enter) && e.Alt) || (e.KeyCode == Keys.F11))
      this.ToggleFullscreen();
}

The FormClosing event handler stores the screen state:

private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
   if (this.FormBorderStyle ==
                        System.Windows.Forms.FormBorderStyle.Sizable)
      Application.UserAppDataRegistry.SetValue("Fullscreen", "false");
   else
      Application.UserAppDataRegistry.SetValue("Fullscreen", "true");
}

And here are the GetScreen and ToggleFullscreen functions:

private Screen GetScreen()
{
   var s = Screen.FromPoint(new Point((int)(this.Left + (this.Width / 2)),
                                    (int)(this.Top + (this.Height / 2))));
   return s ?? Screen.PrimaryScreen;
}

private void ToggleFullscreen()
{
   var s = this.GetScreen();
   if (this.FormBorderStyle ==
       System.Windows.Forms.FormBorderStyle.Sizable)
   {
      this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
      this.Top = s.Bounds.Top;
      this.Left = s.Bounds.Left;
      this.Width = s.Bounds.Width;
      this.Height = s.Bounds.Height;
   }
   else
   {
      this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Sizable;
      this.Top = s.WorkingArea.Top + 40; this.Left =
         s.WorkingArea.Left + 40;
      this.Width = s.WorkingArea.Width - 80; this.Height =
         s.WorkingArea.Height - 80;
   }
 }

Finally, the Resize event handler should invalidate the form, and the Paint event handler should contain the drawing instructions.

Evolutionary sudoku

This is a successful attempt to use an evolutionary algorithm to generate a sudoku board. A complete board is created after usually less than a million iterations, sometimes after a couple of hundred generations.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
   class Program
   {
      static void Main(string[] args)
      {
         var s = new Sudoku();
         s.Display();
         Console.Write("Done. Press enter to quit.");
         Console.ReadLine();
      }
   }

   class Sudoku
   {
      int[,] field = new int[9,9];
      const int mutation_speed = 3;
      const int dead_end_limit = 60000;

      public Sudoku()
      {
         var r = new Random();
         //Create a list of possible characters on the game field.
         var l = new List<int>();
         for (var i = 1; i < 10; i++)
            for (var j = 1; j < 10; j++)
               l.Add(i);
         //Place them on the field randomly.
         for (var i = 0; i < 9; i++)
            for (var j = 0; j < 9; j++) {
               var index = r.Next(0, l.Count);
               var value = l[index]; l.RemoveAt(index);
               this.field[j, i] = value;
            }
         //Prepare for score counting. Low is better.
         int score = int.MaxValue, iterations = 0, generations = 0,
         iterations_since_last_climb = 0;
         //Define how a mutation works - a random element swap.
         Action<int[,]> mutate = a => {
            var speed = (score > 2 ? mutation_speed : 1);
            for (var i = 0; i < speed; i++) {
               int x1 = r.Next(0, 9), y1 = r.Next(0, 9),
                 x2 = r.Next(0, 9), y2 = r.Next(0, 9);
               var v = a[x1, y1]; a[x1, y1] = a[x2, y2]; a[x2, y2] = v;
            }
         };
         //Show progress.
         Action status = () => {
            Console.Title = string
               .Format("Score: {0:00} - Generations: {1:000} - " +
               "Iterations: {2:000000}" +
               " - Since last climb: {3:00000}",
               score, generations, iterations, iterations_since_last_climb);
         };
         //Let evolution work.
         do {
            iterations++;
            //If adaptation has stoped, the parent must mutate.
            if (iterations_since_last_climb >= dead_end_limit) {
               mutate(this.field);
               score = this.GetScore(this.field);
            }
            //Breed two new sudokos and modify them slightly.
            int[,] child1 = (int[,])this.field.Clone();
            int[,] child2 = (int[,])this.field.Clone();
            mutate(child1); mutate(child2);
            //Check new scores.
            var child1score = this.GetScore(child1);
            var child2score = this.GetScore(child2);
            //Keep the best one, if any.
            Action<int[,], int> keep = (a, s) => {
               this.field = (int[,])a.Clone(); score = s; generations++;
               iterations_since_last_climb = 0; };
            if (child1score < score)
               keep(child1, child1score);
            else if (child2score < score)
               keep(child2, child2score);
            else
               iterations_since_last_climb++;
            System.Threading.Thread.Yield();
            if ((iterations % 1000) == 0)
               status();
#if DEBUG
            this.Display();
            Console.CursorTop = 0;
#endif
         } while (score > 0);
         status();
      }

      private int GetScore(int[,] p)
      {
         //Define how fitness is determined. A better solution is wanted.
         Func<int[,], int, int, bool> checkrow = (arr, x, y) => {
            var val = arr[x, y]; var count = 0;
            for (var i = 0; i < 9; i++)
               if (arr[i, y] == val) count++;
            return (count == 1);
         };
         Func<int[,], int, int, bool> checkcol = (arr, x, y) => {
            var val = arr[x, y]; var count = 0;
            for (var i = 0; i < 9; i++)
               if (arr[x, i] == val) count++;
            return (count == 1);
         };
         Func<int[,], int, int, bool> checksection = (arr, x, y) => {
            var section_x = x; while (!((section_x % 3) == 0)) section_x--;
            var section_y = y; while (!((section_y % 3) == 0)) section_y--;
            var val = arr[x, y]; var count = 0;
            for (var row = section_x; row < section_x + 3; row++)
               for (var col = section_y; col < section_y + 3; col++)
                  if (arr[col, row] == val) count++;
            return (count == 1);
         };
         var score = 0;
         for (var i = 0; i < 9; i++)
            for (var j = 0; j < 9; j++) {
               if (!(checkrow(p, j, i))) score++;
               if (!(checkcol(p, j, i))) score++;
               if (!(checksection(p, j, i))) score++;
            }
         return score;
      }

      public void Display()
      {
         for (var y = 0; y < 9; y++) {
            for (var x = 0; x < 9; x++)
               Console.Write(field[x, y]);
            Console.WriteLine();
         }
      }

   }
}

Sudoku