Operatoröverlagring

Inledning

Operatorer kan ges speciell betydelse via överlagring. I vissa fall kan operatoröverlagring ge en snygg, välläslig och bra designad kod men som så många andra tekniker så bör man inte använda operatoröverlagring till överdrift.

Vilka opratorer kan vi då överlagra? Jo det finns en hel del faktiskt:

Operator betydelse
+,-,!,~,++,-- operatorer på en operand
+,-,*,/,%,&,|,^,<<,>> binära operatorer
==, !=, <, >, <=, >= binära operatorer

Vi ska kika lite på hur vi överlagrar operatorerna == och != . Faktum är att dessa, liksom övriga jämförelseoperatorer, måste implementeras i par.

Ett exempel med kort

Vi skapar ett Console Application som döps till OperatorÖverlagring. I detta projekt lägger vi till en struct som vi kallar Card. Denna representation av ett spelkort bygger vi upp av två stycken enum, en för färg (suit) och en för värde (value).

Card.cs

namespace OpertatorÖverlagring
{
    public struct Card
    {
        public Card(CardSuit suit, CardValue value)
        {
            Suit = suit;
            Value = value;
        }

        public readonly CardSuit Suit;
        public readonly CardValue Value;
    }

    public enum CardSuit
    {
        Clubs = 0,
        Diamonds = 1,
        Hearts = 2,
        Spades = 3
    }
    public enum CardValue
    {
        Ace = 1,
        Deuce = 2,
        Three = 3,
        Four = 4,
        Five = 5,
        Six = 6,
        Seven = 7,
        Eight = 8,
        Nine = 9,
        Ten = 10,
        Jack = 11,
        Queen = 12,
        King = 13
    }
}

De publika medlemsvariablerna Suit och Value görs readonly så att de bara kan sättas i konstruktorn och på så vis skyddas från att ändras efter det att ett kort skapats.

Vi passar nu på att skriva några unit-tester för att säkerställa att vår struct Card fungerar som den ska. Om du inte är bekant med unit-tester så är det ingen fara! Använder du någon variant av Visual Studio 2013 så ska det finnas mallar för Microsoft Test, vilket är ett av flera ramverk som kan användas för att skriva unit-tester.

Några unit-tester

I vår solution skapar vi en map tests för våra unit-tester. I denna mapp skapas ett nytt projekt av typen Unit Test Project vilket vi döper till OperatorÖverlagring.Test.

Syftet är att vi i test-projektet ska kunna använda oss av Card och skriva kod som säkerställer att allt fungerar som det ska. För att OperatorÖverlagring.Test ska kunna använda Card så måste vi lägga till en referens till projektet OperatorÖverlagring. När allt förarbete är gjort så ser projektet ut såhär i Visual Studio:

bild

Vi har också passat på att döpa om filen UnitTest1.cs till CardTest.cs.

Nu är det dags för några tester!

CardTest.cs

using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpertatorÖverlagring;

namespace OperatorÖverlagring.Test
{
    [TestClass]
    public class CardTest
    {
        [TestMethod]
        public void SameSuitAndValueShouldEqual()
        {
            var card1 = new Card(CardSuit.Hearts, CardValue.King);
            var card2 = new Card(CardSuit.Hearts, CardValue.King);

            Assert.AreEqual(card1, card2);
            //Assert.IsTrue(card1 == card2);
        }

        [TestMethod]
        public void SameSuitDifferentValuesShouldNotEqual()
        {
            var card1 = new Card(CardSuit.Hearts, CardValue.King);
            var card2 = new Card(CardSuit.Hearts, CardValue.Queen);

            Assert.AreNotEqual(card1, card2);
            //Assert.IsTrue(card1 != card2);
        }

        [TestMethod]
        public void DifferentSuitsSameValueShouldNotEqual()
        {
            var card1 = new Card(CardSuit.Hearts, CardValue.King);
            var card2 = new Card(CardSuit.Spades, CardValue.King);

            Assert.AreNotEqual(card1, card2);
            //Assert.IsTrue(card1 != card2);
        }
    }
}

De bortkommenterade raderna ska vi använda strax! Vi har 3 tester. Ett som testar två likadana kort, ett som testar när färgen skiljer sig men valören är densamma samt ett som testar när valören skiljer sig och färgen är densamma.

Vi kan testa i se vad resultaten blir av testen genom att köra dem. Jag föredrar att använda ReSharper så mitt användargränssnitt kanske skiljer sig från ditt. Testen kan du vanligtvis köra via menyn Test->Run->All tests.

bild

Alla test fungerade! Konstigt?

Skillnader mellan struct och class

Det finns givetvis en förklaring till varför det fungerade som det ska. Eftersom vi använde struct istället för class så deklarerade vi en värdetyp istället för en referenstyp. Både structoch class implementerar metoden Equals( object) med den skillnad att struct gör en "byte för byte"-jämförelse i minnet OM de instanser som jämförs är värdetyp samt INTE innehåller några fält som är av referenstyp.

Låter knepigt? Vår struct Card byggs upp av två enum. Dessa i sin tur är värdetyper liksom vår struct. Alltså sker det en "byte för byte"-jämförelse vilket betyder att om vi har två olika instanser som har samma värden så är de Equals == true.

Skillnad mellan Equals och ==

Är tvungen att poängtera detta! Metoden Equals och operatorn == är INTE samma sak. Va!? Jo, men de används oftast tillsammans just när man gör operatoröverlagring. Equals används för värde-jämförelse och == används för referens-jämförelse. Det är förklaringen till varför koden i testet är bortkommenterad. En struct har ingen implementation för operatorn == men en class har det! En klass gör alltså en referens-jämförelse med operatorn == och får då endast true som resultat om det är samma referens som jämförs.

Ändra till class

Ändra deklarationen av Card från struct till class. Vi kan nu också kommentera av raderna i testet!

Vi kör testerna en gång till:

bild

Nu lyckas inte testet där vi anser att korten är desamma. Eftersom vi bytte till class så gör Equals ingen "byte för byte"-jämförelse längre utan en referens-jämförelse. Nu kan man faktiskt påstå att Equals och == är detsamma.

Override av Equals

Nu lägger vi till en implementation av Equals(object) samt Equals(Card). Det rekommenderas att implementera även Equals(Card) "för ökad prestanda". Det rekommenderas även att implementera System.Object.GetHashCode när man implementerar Equals.

Card.cs - Tillägg 1

        #region Equals

        public override bool Equals(object obj)
        {
            if (obj is Card) return this.Equals((Card)obj);
            else return false;
        }

        public bool Equals(Card other)
        {
            return ((this.Suit == other.Suit) && (this.Value == other.Value));
        }

        public override int GetHashCode()
        {
            return Suit.GetHashCode() ^ Value.GetHashCode();
        }

        #endregion

Nu kör vi testerna igen och sedan diskuterar vi implementationen.

bild

Nu fungerar allt! Detta kräver också en förklaring. Förklaringen är den att implementationen av == använder Equals när det gäller class. Nu gör Equals som vi hade tänkt oss och == råkar göra detsamma.

Riktlinjer för Equals

  • x.Equals(x) returnerar true.
  • x.Equals(y) returnerar samma värde som y.Equals(x).
  • om (x.Equals(y) && y.Equals(z)) returnerar true, då ska x.Equals(z) returnera true.
  • x.Equals(null) returnerar false.
  • Equals ska aldrig generera exceptions.
  • rekommenderad implementation av System.Object.GetHashCode samt Equals(type), som nämnts tidigare.

Nu kanske du har sett en svaghet i implementationen som bryter mot en av punkterna ovan. Gissa vilken!

Du behöver inte tänka allt för länge. Om du kollar på följande test så blir det kanske uppenbart.

Exception i test

	[TestMethod]
	public void NullShouldReturnFalse()
	{
		var card1 = new Card(CardSuit.Hearts, CardValue.King);
		Card card2 = null;

		Assert.IsFalse(card1.Equals(card2));
	}

Override av ==

Om vi nu åter igen byter deklarationen av Card och byter tillbaka till struct så börjar vi närma oss målet. Nu saknas implementationen av == . Vi gör åter igen vår datatyp till en värdetyp och testfallet NullShouldReturnFalse blir inte längre giltigt då en värdetyp inte kan ha värdet null.

Card.cs - Tillägg 2

	#region Operators

	public static bool operator ==(Card value1, Card value2)
	{
		return value1.Equals(value2);
	}

	public static bool operator !=(Card value1, Card value2)
	{
		return !value1.Equals(value2);
	}

	#endregion

Nu kommer vi äntligen till operatoröverlagringen. Denna sker med nyckelordet operator följt av operatorn. Notera också att metoden ska vara public static. Vill man överlagra == så måste även != överlagras. På samma vis gäller t.ex. < och > .

Varning: det är lätt att av misstag använda operatorn == i själva överlagringen. Gör inte det! Du riskerar att få en rekursiv oändlig loop av anrop till == . Använd System.Object.ReferenceEquals för referens-jämförelser eller t.ex (object)a == null om du behöver kolla null-värden.

Nu har vi en struct som fungerar som det är tänkt. Här kan man diskutera om implementationen av Equals verkligen var nödvändig när vi har en struct men visst är det mycket roligare att veta vad man egentligen gör?

Lämna ett svar

Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *

Scroll to top