Permalänk
Medlem

C# yield

Tjenare.

Jag har ett icke-kritiskt problem som jag försöker lösa.
Jag har gjort en funktion som "yieldar" lediga tider som man ska kunna boka i ett bokningsprogram.

Den används på detta vis:

//Lazy load bookings var bookableObjects = getBookableObjects(DateTime.Now);

Då det är en lazy load så får man applicera valfritt filter. Om inget filter appliceras och man kör ToList() så genererar den tider okontrollerat 200 dagar. Ett exempel på ett enkelt filter:

//Hämtar endast bokningsbara tider från och med idag och fem dagar framåt. var bookings = bookableObjects.TakeWhile(x => x.Appointment <= DateTime.Now.AddDays(5)).ToList();

Så långt är allt bra.

På detta vis fungerar den i stort.

public IEnumerable<BookingObjects> getBookableObjects(DateTime startDate) { //Skapa index över bokningsbara objekt, etc. Halvdyr operation, något vi vill helst göra EN gång. //Det sker bara en gång i det första fallet med (x => x.Appointment <= DateTime.Now.AddDays(5)) //Problemet blir när jag applicerar annat filter... mer om det i problembeskrivningen. //Loopa dagens datum och öka med en dag varje steg. Failsafe här på max x antal dagar om man glömmer att applicera ett filter. //Öppna databas och ladda in schemat för alla objekt. //kalla på och yielda getBookableObjectsForDate(schema); }

Då till problemet:

var bookableObjects = getBookableObjects(DateTime.Now); var bookings = bookableObjects.TakeWhile(x => x.Appointment <= bookableObjects.First().Appointment.AddDays(5).ToList();

Det första filtret kommer inte att presentera några objekt om det är fullt mer än fyra dagar framåt, vilket inte är önskvärt.
Detta är ett mycket trevligare filter då den hämtar den senaste bokningsbara tiden samt alla tider 5 dagar efter denna tid.
Ex: Säg att första lediga tiden är om 7 dagar, då får vi den tiden + 5 dagar till.
Med detta filter så kör den hela getBookableObjects() för varje gång den (troligtvis) kör First().
Detta gör alltså att alla bokningsbara tider jämförs med en HEL laddning från första dagen. Praktiskt taget så blir det att First() genererar en full laddning varje gång.

Detta problem kommer jag runt om jag gör på detta vis

var bookableObjects = getBookableObjects(DateTime.Now); var firstBooking = bookableObjects.First(); var bookings = bookableObjects.TakeWhile(x => x.Appointment <= firstBooking.Appointment.AddDays(5).ToList();

Med detta filter så görs det bara en extra laddning av första dagen, för att kunna hämta första lediga tiden.

1. Hur kommer det sig att logiken blir annorlunda för att jag sparar undan variabeln? Jag gissar på att det är för om man kör First() i en lambda/linq så blir det en lazy evaluation varje gång. När jag sparar till en variabel så blir det en lazy evaluation en gång och den gör en vanlig evaluering så jag får resultatet till variabeln.
1.1 Planen längre fram är att jag ska spara undan vissa Querys som man kan applicera på listan. Så punkt 1 blir inte riktig hållbar då om man måste spara undan vissa värden först.
2. Finns det något sätt jag kan komma bort från den extra laddningen helt och hållet? Om man ser till logiken så är det första objektet och First() identiska. Enligt punkt 1 så verkar ju det "logiskt", men kan man komma bort från det? Att den första tiden blir en riktig laddning och ingen lazy evaluation körs?

Det blev ganska mycket text. Fråga på om det är några oklarheter. Det är som sagt inte ett superkritiskt problem då jag kan komma runt det på olika sätt. Men det hade varit snyggt om man kan applicera hela querys för att filtrera resultatet.

Visa signatur

ηλί, ηλί, λαμά σαβαχθανί!?

Permalänk
Medlem

Man kan inte veta att getBookableObjects.First() alltid returnerar samma värde, därför är logiken annorlunda i dina exempel. First() och ditt undansparade objekt behöver inte vara identiska. Tänk dig metoden nedanför, för att vara övertydlig, om jag kallar RandomInts().First() så körs Random.Next() en gång och den returnerar ett värde. Kallar jag den igen returnerar den ett annat värde. Så du har rätt i att din getBookableObjects-metod körs en gång (tills den returnerar värde som ditt filter accepterar, den här gången bara det första värdet) varje gång du kallar First. Men den kör inte "hela" getBookableObjects, den går inte genom alla element. Fast du borde ändå spara undan det första värdet i en variabel innan eftersom du har en dyr operation i getBookableObjects som i ditt fall annars skulle köras flera gånger.

public IEnumerable<int> RandomInts() { while (true) yield return Random.Next(); }

Det korta svaret är att jag tror inte du kan komma runt detta på något annat sätt än att spara undan ett värde utan att ändra på något annat ställe. Finns ju flera andra sätt du kan lösa det på dock. Ledsen att jag inte kan ge dig någon konkret lösning men hoppas det i alla fall gav lite klarhet i varför problemet finns (om du inte redan har koll, kanske bara jag som missförstod dig).

Permalänk
Medlem
Skrivet av Goose7:

Man kan inte veta att getBookableObjects.First() alltid returnerar samma värde, därför är logiken annorlunda i dina exempel. First() och ditt undansparade objekt behöver inte vara identiska. Tänk dig metoden nedanför, för att vara övertydlig, om jag kallar RandomInts().First() så körs Random.Next() en gång och den returnerar ett värde. Kallar jag den igen returnerar den ett annat värde. Så du har rätt i att din getBookableObjects-metod körs en gång (tills den returnerar värde som ditt filter accepterar, den här gången bara det första värdet) varje gång du kallar First. Men den kör inte "hela" getBookableObjects, den går inte genom alla element. Fast du borde ändå spara undan det första värdet i en variabel innan eftersom du har en dyr operation i getBookableObjects som i ditt fall annars skulle köras flera gånger.

public IEnumerable<int> RandomInts() { while (true) yield return Random.Next(); }

Det korta svaret är att jag tror inte du kan komma runt detta på något annat sätt än att spara undan ett värde utan att ändra på något annat ställe. Finns ju flera andra sätt du kan lösa det på dock. Ledsen att jag inte kan ge dig någon konkret lösning men hoppas det i alla fall gav lite klarhet i varför problemet finns (om du inte redan har koll, kanske bara jag som missförstod dig).

Tackar för svar. Jag kan hålla med dig i din argumentation. Det går inte att garantera att första (eller andra, tredje, osv) är identiska. Problemet är att i mitt fall så är de helt identiska, men jag förstår att lazy loaden inte bryr sig om det. Jag har klurat lite mer på det och kanske om jag blandar in index på något sätt, så kanske den förstår att alla med samma index är identiska? Jag vet inte hur det ska se ut riktigt, men jag tror att det finns vissa querys man kan skriva som tar hänsyn till index i listan istället.

Den enda anledningen till varför jag yieldar resultaten är för att jag vill ha en "infinite" lista av tider som endast begränsas av filtret jag applicerar. Det är nästan ett måste för att om det inte finns lediga tider i närheten så vill vi ju ändå returnera ett gäng tider från och med när det blir ledigt.

Jag kan lägga till att First() är värre än jag gissade. Ponera att första lediga tid är om 5 veckor. Då kommer metoden köra 5*7 gånger för First() och sen 5*7 gånger för att hitta första "x" i lambda-uttrycket. Andra "x" kommer fortsätta efter 5*7 så då blir det "billigt" igen. För att hitta bara EN tid så blir det i det fallet (5*7)^2 laddningar.

Lite synd är det men som sagt, det är inte kritiskt. Hela flödet är upplagt med progressbar osv. Det är inget som förväntas ta en halv sekund. Det ligger på trevliga 3-5 sekunder i dagsläget vilket är helt klart godkänt med tanke på vilka querys och urval jag gör just nu.

Jag kör på den sparade variabeln tillsvidare.

Visa signatur

ηλί, ηλί, λαμά σαβαχθανί!?

Permalänk
Medlem
Skrivet av Leedow:

Tackar för svar. Jag kan hålla med dig i din argumentation. Det går inte att garantera att första (eller andra, tredje, osv) är identiska. Problemet är att i mitt fall så är de helt identiska, men jag förstår att lazy loaden inte bryr sig om det. Jag har klurat lite mer på det och kanske om jag blandar in index på något sätt, så kanske den förstår att alla med samma index är identiska? Jag vet inte hur det ska se ut riktigt, men jag tror att det finns vissa querys man kan skriva som tar hänsyn till index i listan istället.

Den enda anledningen till varför jag yieldar resultaten är för att jag vill ha en "infinite" lista av tider som endast begränsas av filtret jag applicerar. Det är nästan ett måste för att om det inte finns lediga tider i närheten så vill vi ju ändå returnera ett gäng tider från och med när det blir ledigt.

Jag kan lägga till att First() är värre än jag gissade. Ponera att första lediga tid är om 5 veckor. Då kommer metoden köra 5*7 gånger för First() och sen 5*7 gånger för att hitta första "x" i lambda-uttrycket. Andra "x" kommer fortsätta efter 5*7 så då blir det "billigt" igen. För att hitta bara EN tid så blir det i det fallet (5*7)^2 laddningar.

Lite synd är det men som sagt, det är inte kritiskt. Hela flödet är upplagt med progressbar osv. Det är inget som förväntas ta en halv sekund. Det ligger på trevliga 3-5 sekunder i dagsläget vilket är helt klart godkänt med tanke på vilka querys och urval jag gör just nu.

Jag kör på den sparade variabeln tillsvidare.

Index skulle bli precis samma sak som First() tror jag. Ett problem där är ju att en IEnumerable är inte (nödvändigtvis) en lista, så vad som kommer att hända är att en metod som ElementAt() kallas istället och den kommer göra precis som First(), av samma anledning. Den måste kalla getBookableObjects() för att få veta vad som finns vid ett specifikt index. För att det ska fungera måste du kalla ToList() först och då skulle ju såklart även First() att vara mycket snabbare.

Om det är något specifikt till funktionen du beskrev innan kan du ju alltid bygga in funktionaliteten i en getBookableObjectsAfterFirstWithin(TimeSpan time)-metod som returnerar en IEnumerable med alla lediga tider inom "time" efter den första lediga (eller någon lösning åt det hållet, du kanske hittar på något stiligare). Med en sådan lösning slipper du ju köra din dyra operation i getBookableObjects två gånger i alla fall.

Permalänk
Medlem

Kan du inte lägga en statisk variabel som kollar om den är körd på det ena viset först och då skippar andra körningen och nollar variabeln?

Visa signatur

"Om man arbetar tillräckligt länge med att förbättra ett föremål går det sönder. "

Hjälp oss göra världen lite snällare! www.upphittat.nu

Permalänk
Medlem
Skrivet av Goose7:

Index skulle bli precis samma sak som First() tror jag. Ett problem där är ju att en IEnumerable är inte (nödvändigtvis) en lista, så vad som kommer att hända är att en metod som ElementAt() kallas istället och den kommer göra precis som First(), av samma anledning. Den måste kalla getBookableObjects() för att få veta vad som finns vid ett specifikt index. För att det ska fungera måste du kalla ToList() först och då skulle ju såklart även First() att vara mycket snabbare.

Om det är något specifikt till funktionen du beskrev innan kan du ju alltid bygga in funktionaliteten i en getBookableObjectsAfterFirstWithin(TimeSpan time)-metod som returnerar en IEnumerable med alla lediga tider inom "time" efter den första lediga (eller någon lösning åt det hållet, du kanske hittar på något stiligare). Med en sådan lösning slipper du ju köra din dyra operation i getBookableObjects två gånger i alla fall.

Hmm ok, vad synd.
Ja, "dyra" operationen kan jag göra före allt och skicka med resultatet till getBookableObjects(). Men fortfarande blir det dyrt (5*7-exemplet i förra posten) då First() gör laddar dagar (5*7) och hittar första och returnerar den. Uttrycket i lambda gör samma sak, skillnaden är att den sedan fortsätter därifrån och presenterar tider vidare. Det är alltså 5*7 laddningar i "onödan".

Medan jag skrev detta så blev jag sugen på att testa en grej som jag kom på. Det är egentligen jobb detta och det är helg, men det är roliga problem.
Detta löser problemet! Inte speciellt vackert, kanske går att göra snyggare men jag blir av med den extra sökningen som First() innebär. Det känns som att det måste gå att göra mycket snyggare. Typ som om jag har glömt något eller kan skriva om filtreringen med annat än TakeWhile, etc.

private void getBookings() { var bookableObjects = getBookableObjects(DateTime.Now); var bookings = bookableObjects.TakeWhile(x => hasAppointmentEarlierThan(x,5)).ToList(); } BookingObject first = null; private bool hasAppointmentEarlierThan(BookingObject bo, int days) { if (first == null) first = bo; if (bo.Appointment <= first.Appointment.AddDays(days)) return true; return false; }

Visa signatur

ηλί, ηλί, λαμά σαβαχθανί!?

Permalänk
Medlem
Skrivet av ZecretW:

Kan du inte lägga en statisk variabel som kollar om den är körd på det ena viset först och då skippar andra körningen och nollar variabeln?

Hmm... jag vet inte riktigt hur du menar.
Har du lust att utveckla ditt svar lite mer?

Visa signatur

ηλί, ηλί, λαμά σαβαχθανί!?

Permalänk
Medlem
Skrivet av Leedow:

Hmm... jag vet inte riktigt hur du menar.
Har du lust att utveckla ditt svar lite mer?

Jag kanske har missuppfattat detta helt. Kan du inte lagra första körningens resultat internt och när du frågar efter samma sak igen så returnerar du bara det du sparat. Om du inte frågar efter samma sak så får du naturligtvis göra en ny körning.

Visa signatur

"Om man arbetar tillräckligt länge med att förbättra ett föremål går det sönder. "

Hjälp oss göra världen lite snällare! www.upphittat.nu

Permalänk
Medlem

Problemet som jag ser det är att funktionen getBookableObjects är dåligt skriven för det du vill göra. Jag antar att den hämtar ett set från en databas och det du vill göra är en relativt enkel slq query.

Så som du använt den antar jag att funktionen är något i stil med

public list<BookableObject> getBookableObjects(DateTime fromDate)

Om du har möjlighet så gör en överlagrad version som även tar en variabel numberOfDays

public list<BookableObject> getBookableObjects(DateTime fromDate, int numberOfDays)

och plocka fram det du vill ha direkt i sql-queryn istället.

Och dessutom kan man som ZecretW säger använda caching för att spara resultatet om samma anrop sker många gånger.

Det är inget fel på din lösning. Den är säkert mer än tillräcklig för ändamålet. Men du verkar ju vilja optimera och då anser jag att databasen i normalfallet gör selektering snabbast. Gör rätt anrop mot databasen och få rätt urval på en gång så vinner du mycket.

Visa signatur

He who hasn't hacked assembly language as a youth has no heart. He who does so as an adult has no brain.
~John Moore

Permalänk
Medlem
Skrivet av ZecretW:

Jag kanske har missuppfattat detta helt. Kan du inte lagra första körningens resultat internt och när du frågar efter samma sak igen så returnerar du bara det du sparat. Om du inte frågar efter samma sak så får du naturligtvis göra en ny körning.

Om jag förstår dig rätt så gör den senaste koden jag visade just så. Den kör filtreringen och försöker hitta den första. Om "first"-variabeln inte är satt så sätter den nuvarande till "first". Då slipper man den extra laddningen som First() gjorde.

Skrivet av Anaii:

Problemet som jag ser det är att funktionen getBookableObjects är dåligt skriven för det du vill göra. Jag antar att den hämtar ett set från en databas och det du vill göra är en relativt enkel slq query.

Så som du använt den antar jag att funktionen är något i stil med

public list<BookableObject> getBookableObjects(DateTime fromDate)

Om du har möjlighet så gör en överlagrad version som även tar en variabel numberOfDays

public list<BookableObject> getBookableObjects(DateTime fromDate, int numberOfDays)

och plocka fram det du vill ha direkt i sql-queryn istället.

Och dessutom kan man som ZecretW säger använda caching för att spara resultatet om samma anrop sker många gånger.

Det är inget fel på din lösning. Den är säkert mer än tillräcklig för ändamålet. Men du verkar ju vilja optimera och då anser jag att databasen i normalfallet gör selektering snabbast. Gör rätt anrop mot databasen och få rätt urval på en gång så vinner du mycket.

Den är superoptimerad som den är. Själva laddningen av schemat har optimerats i flera år av flera utvecklare.
Den laddar alla resurser, kompetenser, schematid, bokningar etc och bygger en modell i C# över hur dagen ser ut för alla resurser. Den traverserar sedan modellen och kan på så sätt presentera lediga tider eller vad man nu vill presentera. Väldrigt bekväm objektorientering för att göra vad som helst med schemat. Jag skulle aldrig göra detta direkt mot databasen.

Det är inte en List<BookableObject> då det är yieldat. Det går inte att yielda en List. Det är alltså en:
public IEnumerable<BookableObject> getBookableObjects() { }

Varför det yieldas är för att jag gör en massa olika filtreringar på de tiderna som presenteras och vill inte spara undan tider som jag ändå inte ska presentera. Hade jag kört en lista så hade listan blivit enormt stor. ToList() när jag är klar skapar en List av faktiska tider som är "lämpliga". Datumfiltreringen är bara ett av många filter som körs och jag vill bara förhindra att exakt samma BookingObject (resurs och tidmässigt, inte identitetsmässigt) laddas fler än två gånger.

Visa signatur

ηλί, ηλί, λαμά σαβαχθανί!?

Permalänk
Medlem
Skrivet av Leedow:

Hmm ok, vad synd.
Ja, "dyra" operationen kan jag göra före allt och skicka med resultatet till getBookableObjects(). Men fortfarande blir det dyrt (5*7-exemplet i förra posten) då First() gör laddar dagar (5*7) och hittar första och returnerar den. Uttrycket i lambda gör samma sak, skillnaden är att den sedan fortsätter därifrån och presenterar tider vidare. Det är alltså 5*7 laddningar i "onödan".

Medan jag skrev detta så blev jag sugen på att testa en grej som jag kom på. Det är egentligen jobb detta och det är helg, men det är roliga problem.
Detta löser problemet! Inte speciellt vackert, kanske går att göra snyggare men jag blir av med den extra sökningen som First() innebär. Det känns som att det måste gå att göra mycket snyggare. Typ som om jag har glömt något eller kan skriva om filtreringen med annat än TakeWhile, etc.

private void getBookings() { var bookableObjects = getBookableObjects(DateTime.Now); var bookings = bookableObjects.TakeWhile(x => hasAppointmentEarlierThan(x,5)).ToList(); } BookingObject first = null; private bool hasAppointmentEarlierThan(BookingObject bo, int days) { if (first == null) first = bo; if (bo.Appointment <= first.Appointment.AddDays(days)) return true; return false; }

Det jag tänkte var snarare något i stil med:

public IEnumerable getBookableObjects(DateTime time, TimeSpan numberOfDaysAfterFirstAvailableTime) { //Dyr operation //Här får du tillgång till first //yield return värden som du är intresserade av, till exempel de som är inom specificerat antal dagar efter first

Alltså ungefär som Anaii skrev, en överlagrad version av getBookableObjects. Det blir ju något mer effektivt än att först spara undan first (utanför metoden) eftersom att du bara behöver köra din dyra operation en gång.

Permalänk
Medlem
Skrivet av Goose7:

Det jag tänkte var snarare något i stil med:

public IEnumerable getBookableObjects(DateTime time, TimeSpan numberOfDaysAfterFirstAvailableTime) { //Dyr operation //Här får du tillgång till first //yield return värden som du är intresserade av, till exempel de som är inom specificerat antal dagar efter first

Alltså ungefär som Anaii skrev, en överlagrad version av getBookableObjects. Det blir ju något mer effektivt än att först spara undan first (utanför metoden) eftersom att du bara behöver köra din dyra operation en gång.

Ja, det är ett alternativ. Jag ska testa det vid tillfälle.
Som jag skrev tidigare så kan den "dyra operationen" göras innan och resultatet av detta skickas med in till getBookableObjects(). Vad som fortfarande blir dyrt är att leta fram nästa lediga tid två gånger (vilket är vad som sker i ursprungsinlägget men är åtgärdat i mitt sista inlägg som innehåller kod). Det låter ju helt klart vettigt att inte behöva mecka med detta utanför metoden så det är värt ett försök.

Visa signatur

ηλί, ηλί, λαμά σαβαχθανί!?