Ogromna większość technologii programistycznych (języków programowania, języków skryptowych itp.) zakłada specyficzny sposób przetwarzania wyrażeń logicznych zawartych najczęściej w różnego typu instrukcjach warunkowych (IF, WHILE, FOR). Chodzi o to, że zdanie logiczne składające się z kilku wyrażeń, np:
A OR B OR C
jest uwzględniane tylko do momentu, w którym da się określić wartość całego wyrażenia. W powyższym przykładzie moment ten nastąpi już w sytuacji, w której wartość logiczna wyrażenia A będzie prawdziwa (alternatywa jest prawdziwa, gdy prawdziwy jest jeden z jej elementów). Głównym uzasadnieniem takiej obsługi są kwestie wydajnościowe i praktyczne. Opisane podejście do przetwarzania warunków ma jednak szereg konsekwencji, które warto sobie uświadomić. Postaram się zwrócić na nie uwagę bardziej szczegółowo prezentując implementację zasygnalizowanego zagadnienia w kilku środowiskach.
Practical Extraction and Report Language (Perl)
Spójrzmy na poniższy skrypt:
#!/usr/bin/perl
use strict;
use warnings;
my $count = 0;
my $a = 1;
if ($a != 1 and func() == 2) {
}
print $count;
sub func
{
$count++;
return 2;
}
Interesuje nas odpowiedź na pytanie, czy funkcja
func() zostanie w ogóle wywołana. Otóż - zgodnie z tym, co napisałem we wstępie - nie zostanie. Nie ma potrzeby sprawdzania wyrażenia z funkcją, gdyż już na podstawie nieprawdziwości wyrażenia
$a != 1 (1 != 1) można stwierdzić, że całe zdanie warunku jest fałszywe. Skrypt drukuje więc:
0
Trzeba o tym pamiętać umieszczając funkcję w logice instrukcji warunkowych.
W rzeczywistości to właśnie opisana obsługa warunków umożliwia stosowanie konstrukcji typu:
if (defined $zmienna and $zmienna == 5) { }
Gdyby za każdym razem przetwarzane było całe wyrażenie, to przy zastosowanym
use warnings; dochodziłoby do monitu:
Use of uninitialized value in numeric eq (==) at (...)
zawsze wtedy, kiedy
$zmienna byłaby undef.
Platforma .NET (C#)
Obsługa jest analogiczna, poniższy program drukuje 0.
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApplication1
{
class Program
{
public static int count = 0;
public static int func()
{
count++;
return 2;
}
static void Main(string[] args)
{
int a = 1;
if (a == 1 || func() == 2)
{
}
System.Console.WriteLine(count);
}
}
}
Podobnie jak w Perlu, w C# (i innych językach .NETowskich) dzięki specyficznej obsłudze wyrażeń logicznych możliwe jest stosowanie konstrukcji:
if (obj != null && obj.property == 2) { }
Różnica między C# i Perlem polega jednak w tym przypadku na tym, że o ile w Perlu użycie niezainicjalizowanej zmiennej skutkuje co najwyżej ostrzeżeniem (warning level), o tyle w .NET użycie nieistniejącego obiektu kończy się rzuceniem wyjątku
NullReferenceException().
C/C++
Przykładowy kod w przypadku C:
#include < stdio.h>
void main()
{
int count = 0;
int a = 1;
if (a == 1 || func(&count) == 2) { }
printf("%d", count);
}
int func(int* pcount) {
(*pcount)++;
return 2;
}
Jeżeli w powyższym kodzie pierwsze wyrażenie składowe warunku IF zostanie zmienione na
1 == 1, to większość kompilatorów w ogóle nie uwzględni drugiego wyrażenia alternatywy (z funkcją
func()) - skompiluje więc warunek jako zawsze prawdziwy. Można się o tym łatwo przekonać zamieniając dodatkowo nazwę funkcji w warunku na taką, która nie istnieje:
if (1 == 1 || nie_mam_definicji(&count) == 2) { }
W obu przypadkach (zmieniony i niezmieniony kod przykładowy) skompilowany program zwraca 0.
JavaScript
Obsługa jest analogiczna:
<SCRIPT TYPE="text/javascript">
var count = 0;
var a = 1;
function func() {
count++;
return 2;
}
if (a == 1 || func() == 2) { }
alert(count);
</SCRIPT>
Alert prezentuje wartość 0.
Microsoft SQL Server
Jeżeli wywołamy poniższy T-SQL:
SELECT 0/0
SQL Server natychmiast rzuci błędem:
Msg 8134, Level 16, State 1, Line 1
Divide by zero error encountered.
Kiedy jednak dzielenie przez 0 znajdzie siÄ™ w odpowiednim warunku logicznym:
IF (1=1 OR 1=0/0)
PRINT 'TRUE'
błąd nie występuje, gdyż nie dochodzi do wykonania wadliwego kodu.
O ile w przypadku Perla, C czy C# specyficzna ewaluacja warunków logicznych jest czymś pozytywnym, o tyle w świecie baz danych prowadzi do pewnych niekonsekwencji. Trzeba pamiętać, że SQL Server i inne RDBMS implementują logikę trójwartościową (three-valued logic, 3VL). Przestrzeń nie ogranicza się więc do wartości TRUE i FALSE, lecz składa się na nią TRUE, FALSE i UNKNOWN. Może więc dojść do sytuacji, że kiedy - ze względu na operację z NULLem - warunek powinien mieć wartość logiczną UNKNOWN, w praktyce zwraca wartość prawdy. Dokładnie taka sytuacja ma miejsce w poniższym przykładzie:
DECLARE @a int;
SET @a = 1;
IF (NOT (@a<>1 AND 1=NULL))
PRINT 'TRUE';
Warto o tym pamiętać, gdyż NULLe i stany nieokreśloności mogą generować sporą ilość trudno wykrywalnych problemów.
Sposób ewaluacji wyrażeń logicznych jest analogiczny w stosunku do wyżej opisanego w przypadku wszystkich znanych mi technologii (języków). Wiedza o takim sposobie obsługi jest więc całkiem wskazana tym bardziej, że umożliwi ona zapisywanie skomplikowanych warunków logicznych w sposób bardziej wydajny - czyli taki, aby sterując kolejnością wyrażeń składowych jak najwcześniej dało się zaprzeczyć lub uznać za prawdziwy cały warunek. Oczywiście ten swoisty tuning wyrażeń logicznych może mieć miejsce wyłącznie w środowisku, które respektuje kolejność wykonywania wyrażeń składowych zgodnie z ich występowaniem w kodzie (od lewej do prawej). Jest to powszechny standard, ale np. SQL Server się do niego nie stosuje.