Programiści, którzy “zasmakowali” pracy w językach wysokiego poziomu, takich jak m. in. PHP, bardzo często zapominają o możliwości wykorzystania bardzo niskich mechanizmów do osiągnięcia większej elastyczności kodu. Z reguły wykorzystujemy w kodzie różne wzorce projektowe i inne ułatwienia wprowadzone wraz z paradygmatem obiektowym programowania i myślimy za pomocą dużych komponentów, zamiast skorzystać z tego, co istnieje i ma się dobrze od co najmniej kilkunastu lat. Niniejszy wpis będzie poświęcony jednej z takich właśnie możliwości - wykorzystaniu flag bitowych. Zapraszam do lektury.

Fotografia: Bull3t, CC-BY-SA.

Problem: duża ilość parametrów.

Ci, którzy mieli styczność z językiem C czy C++ na pewno zetknęli się kiedyś z problemem przekazywania ustawień czy opcji do odpowiedniej funkcji. Weźmy pod uwagę przykładowy kod:

/**
 * Trim passed string using selected method.
 *
 * @param string $string Passed string.
 * @param bool $trimLeft Trim left side.
 * @param bool $trimRight Trim right side.
 * @param bool $trimBoth Trim both sides.
 * @return string Trimmed string.
 */
function customTrim($string, $trimLeft, $trimRight, $trimBoth)
	{
	$result = '';
	if(true == $trimLeft)
		{
		$result = ltrim($str);
		}
	else if(true == $trimRight)
		{
		$result = rtrim($string);
		}
	else if(true == $trimBoth)
		{
		$result = trim($string);
		}
	return $result;
	}

Funkcja ta nie jest specjalnie ambitna, ale dosyć dobrze pokazuje problem - w momencie, kiedy chcielibyśmy, aby jej działanie było sterowane za pomocą kilku binarnych przełączników musimy umieścić w deklaracji funkcji bardzo dużą liczbę parametrów, z których tylko jeden lub kilka ma faktyczne znaczenie i wpływ na wartość wynikową. Oczywiście możemy zmienić listę parametrów na przekazanie jednej tablicy:

/**
 * Trim passed string using selected method.
 *
 * @param string $string Passed string.
 * @param array $options Trim options.
 * @return string Trimmed string.
 */
function customTrim($string, $options)
	{
	$result = '';
	if(true == $options['trimLeft'])
		{
		$result = ltrim($str);
		}
	else if(true == $options['trimRight'])
		{
		$result = rtrim($string);
		}
	else if(true == $options['trimBoth'])
		{
		$result = trim($string);
		}
	return $result;
	}

Jednakże w tym przypadku alokujemy wręcz monstrualną ilość pamięci do tak prostej czynności jak wykonanie jednego z wariantów funkcji trim().

Flagi bitowe: najlepsza kompresja przekazywanych informacji.

Czym są flagi bitowe? Otóż, jak każdy wie - najmniejszą jednostką pamięci komputera dostępną dla programisty jest bit. 8 bitów stanowi 1 bajt, a dalej już wchodzą “standardowe” modyfikatory ilości - kilobajt, megabajt, gigabajt, itd. Większość programistów nie zważa na ilość pamięci, jaką zajmują ich programy / skrypty, stąd nie zauważa także potencjału tkwiącego w pojedynczej zmiennej zawierającej po prostu… liczbę całkowitą. Zauważmy, że w przypadku zwykłej zmiennej typu int, zajmującej 1 bajt w pamięci mamy aż 8 możliwych stanów - przełączników dla każdego bitu:

1: 00000001
173: 10101101
255: 11111111

Każdy z tych stanów możemy odczytać z liczby z pomocą operatora bitowego AND:

define('TRIM_LEFT', 1);   // 00000001
define('TRIM_RIGHT', 2);  // 00000010
define('TRIM_BOTH', 4);   // 00000100

$state = 173;             // 10101101
echo $state & TRIM_LEFT;  // 1
echo $state & TRIM_RIGHT; // 0
echo $state & TRIM_BOTH;  // 4

Flaga musi być potęgą dwójki, tzn. musi zawierać tylko jedną “jedynkę” w reprezentacji binarnej. Wynikiem operacji jest liczba odpowiadająca konfiguracji bitów, jakie były ustawione na tych samych pozycjach w obu liczbach, a więc w zależności od tego, czy trafimy w odpowiednią “jedynkę” zostanie zwrócona wartość flagi lub zero.

Aby nie musieć za każdym razem przemnażać kolejnych flag przez 2, możemy też wykorzystać operator przesunięcia bitowego. W taki przypadku definiujemy tylko pierwszą flagę o wartości jeden, a następnie “przesuwamy jedynkę” o kolejną ilość miejsc w prawo:

define('TRIM_LEFT', 1);
define('TRIM_RIGHT', TRIM_LEFT << 1);
define('TRIM_BOTH', TRIM_LEFT << 2);

Spróbujmy wykorzystać te informacje, aby zmienić naszą funkcję customTrim() w nieco bardziej konfigurowalny twór.

Rozwiązanie: parametr zawierający flagi bitowe.

Wykorzystując flagi bitowe zdefiniowane wyżej kod funkcji będzie wyglądał następująco:

/**
 * Trim passed string using selected method.
 *
 * @param string $string Passed string.
 * @param int $flags Trim options.
 * @return string Trimmed string.
 */
function customTrim3($string, $flags)
	{
	$result = $string;
	if($flags &amp; TRIM_LEFT)
		{
		$result = ltrim($result);
		}
	if($flags &amp; TRIM_RIGHT)
		{
		$result = rtrim($result);
		}
	if($flags &amp; TRIM_BOTH)
		{
		$result = trim($result);
		}
	return $result;
	}

Jak widać, zyskujemy zarówno na ilości parametrów [przekazujemy tylko string i flagi], jak i na pamięci [cały parametr zawierający flagi to… pojedynczy “int”]. Mówiąc o optymalizacji za pomocą flag bitowych wypada także powiedzieć kilka słów na temat sposobu wywołania takiej funkcji. Aby stworzyć wartość odpowiadającą odpowiedniemu zbiorowi flag posłużymy się operatorem binarnym OR:

echo 'x'.customTrim3('  STR STR   ', TRIM_LEFT).'x'."\n";
echo 'x'.customTrim3('  STR STR   ', TRIM_RIGHT).'x'."\n";
echo 'x'.customTrim3('  STR STR   ', TRIM_LEFT | TRIM_RIGHT).'x'."\n";
echo 'x'.customTrim3('  STR STR   ', TRIM_BOTH).'x'."\n";

// result:
xSTR STR   x
x  STR STRx
xSTR STRx
xSTR STRx

A zatem aby przekazać kilka wartości poszczególnych przełączników należy je po prostu połączyć operatorem binarnym OR.

BTW. Zdaję sobie sprawę z tego, że omówiony przeze mnie przykład jest “mocno średni”, dlatego jeśli macie nieco lepszy pomysł na funkcję przedstawiającą opisywany problem, proszę podzielcie się nimi w komentarzach. Do zobaczenia w piątek!