Xpoint
   [напомнить пароль]

Тернарный оператор (if/then/else) средствами XPath

Оглавление

Проблема

Постановка задачи

Нужен нормальный аналог оператора ($cond) ? ($one) : ($two) в рамках XPath 1.0.

Стандартное решение через xsl:choose и его недостатки

Подобные задачи в XSLT традиционно решают так:

<xsl:choose>
  <xsl:when test='$cond'>
    <xsl:copy-of select='$one'/>
  </xsl:when>
  <xsl:otherwise>
    <xsl:copy-of select='$two'/>
  </xsl:otherwise>
</xsl:choose>

Однако это решение, помимо очевидной громоздкости, обладает еще одним, весьма существенным недостатком: то, что получается в результате — RTF. Т.е. результат такого выражения приемлем в выходном потоке, но никак не может быть использован (по крайней мере в рамках "чистого" XSLT 1.0, без расширений) для последующих преобразований.

Конечно, с помощью exsl:node-set (или ее аналога), мы можем превратить этот RTF в полноценное множество узлов, но останется одна проблема: это будет новое множество узлов, никак не связанное с исходными документами. Узел, полученый в результате преобразования, будет его корневым узлом — применить к нему, к примеру, ось ancestor мы уже не сможем.

Попробуем преодолеть этот недостаток.

Решение на чистом XPath 1.0

Решение

Воспользуемся тем фактом, что при задании осей выборки мы можем использовать предикаты, налагающие на них ограничивающие условия: $one[$cond] | $two[not($cond)].

Его недостатки

Это выражение лишено недостатков "классического" варианта, но у него есть свои, «дополняющие» их:

  1. Если xsl:choose нельзя (кросс-процессорным способом) использовать для получения node-set'ов, то этот вариант, напротив, может работать только с node-sets в качестве как параметров, так и результата.

Т.е. приведенный пример будет работать только если $one и $two — множества узлов. Запихнуть в это выражение строку или число уже не получится.

  1. Если в примере вместо переменной $cond использовать некоторое сложное выражение, результат его вычисления может зависеть от содержимого $one и $two.

Например $one[count(.) < $max] | $two[count(.) >= $max] — значение count(.) будет разным в обоих случаях, и совсем не тем, которое, очевидно, имел бы ввиду гипотетический горе-автор такого выражения...

  1. Исходное выражение достаточно сложно для понимания. Когда я привел пример его употребления в форуме, мне приходилось слышать замечания, что «вообще-то | в xslt — это не or, а объединение нодесетов...»

Совершенно верно, но or в XPath работает отнюдь не так, как привыкли пользователи большинства современных языков. Оно возвращает результат уже преобразованным в логический тип, что делает его совершенно непригодным для данной задачи. И здесь нужно использовать именно объединение с последующей проверкой, хоть это и не очевидно.

  1. При обработке этого выражения вычисляются обе ветки и лишь затем проверяется их соответствие условию. Хотя это почти никогда не вызывает проблем (выражения XSLT не должны иметь побочных действий) — иногда это все же имеет значение.

Утверждение «никакие функции в XSLT не имеют побочных эффектов»неверно. Помимо функций расширений (а наличие у них побочных эффектов вполне возможно), есть по крайней мере одна такая функция, входящая в спецификацию XSLT 1.0. Это функция document: действительно, вряд ли кто-нибудь станет утверждать, что вызов document('http://example.com/cgi-bin/counter.pl') побочных эффектов иметь не будет...

Заметим сразу, что преодолеть эти недостатки этого выражения в рамках «чистого» XSLT 1.0 (т.е. без расширений) нельзя. В этом случае остается лишь предложить использовать этот вариант, если нам нужен на выходе node-set, и "классический" — во всех остальных случаях.

Использование расширений EXSLT

Если же снизить рамки требований по совместимости, и допустить использование EXSLT, то нам представляется отличная возможность написать собственную функцию, эмулирующий тернарный оператор, лишенный большей части указанных недостатков.

Для создания функций расширений EXSLT предоставляет элементы func:function и func:result, где префикс func: соответствует xmlns http://exslt.org/functions.
Первый из них практически точно соответствует элементу XSLT 2.0 xsl:function. Аналога же result там нету, для создания возвращаемых значений используется стандартный синтаксис коструктора последовательности.

Решение для общего случая

Итак, определим собственную функцию расширения (lib: — префикс нашего собственного пространства имен, допустим — urn:xslt:library):

<func:function name='lib:if'>
  <xsl:param name='cond'/>
  <xsl:param name='then'/>
  <xsl:param name='else' select='false()'/>
  
  <xsl:choose>
    <xsl:when test='$cond'>
      <func:result select='$then'/>
    </xsl:when>
    <xsl:otherwise>
      <func:result select='$else'/>
    </xsl:otherwise>
  </xsl:choose>
</func:function>

Наш пример с ее помощью запишется так: lib:if($cond, $one, $two).

Если есть побочные эффекты

Приведенный выше варинат лишен многих недостатков, присущих стандартным решениям. Он компактен, прост для понимания, его можно использовать с аргументами любого типа, вычисление $cond происходит в момент вызова функции. Однако осталась одна проблема: оба остальных параметра тоже вычисляются в момент вызова функции.

В большинстве случаев этим можно пренебречь. Но если наши аргументы имеют побочные эффекты — это недопустимо. Потому воспользуемся для решения этой задачи еще одной функцией EXSLT — dyn:evaluate, где xmlns:dyn = "http://exslt.org/dynamic". Она, как нетрудно догадаться, получает в качестве аргумента строку и вычисляет ее как выражение XPath непосредственно в том контексте, в котором вызвана:

<func:function name='lib:if-dyn'>
  <xsl:param name='cond'/>
  <xsl:param name='then'/>
  <xsl:param name='else' select='"false()"'/>
  
  <xsl:choose>
    <xsl:when test='$cond'>
      <func:result select='dyn:evaluate($then)'/>
    </xsl:when>
    <xsl:otherwise>
      <func:result select='dyn:evaluate($else)'/>
    </xsl:otherwise>
  </xsl:choose>
</func:function>

Таким образом, любое выражение, которое мы передадим этой функции — в виде строки, не следует забывать об этом — будет вычислено лишь в том случае, если оно действительно используется. Но следует отметить, что lib:if, вообще говоря, должна выполняться быстрее (и не требовать еще одного модуля расширений) — поэтому, если нам не нужна эта дополительная функциональность, всегда следует использовать lib:if.

Кроме того, libxslt, например, требует, чтобы http://exslt.org/dynamic было определено в точке вызова lib:if-dyn, что уже совсем никуда не годится...

Пример

Приведем полный пример, иллюстрирующий разницу между функциями:

lib/if.xsl:

<?xml version="1.0"?>

<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:func="http://exslt.org/functions"
  xmlns:dyn="http://exslt.org/dynamic"
  xmlns:lib="urn:xslt:library"
  extension-element-prefixes="func dyn lib"
 >

<func:function name='lib:if'>
  <!-- см. выше -->
</func:function>

<func:function name='lib:if-dyn'>
  <!-- см. выше -->
</func:function>

</xsl:stylesheet>

example.xsl:

<?xml version="1.0"?>

<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:dyn="http://exslt.org/dynamic"
  xmlns:lib="urn:xslt:library"
  extension-element-prefixes="dyn lib"
 >
<xsl:import href='lib/if.xsl'/>

<!-- использование lib:if -->
<xsl:variable name='one' 
  select="lib:if(true(), document('one/foo.xml'), document('one/bar.xml'))"
/>

<!-- использование lib:if-dyn, 
обратите внимание, что аргументы заключены в кавычки -->
<xsl:variable name='one' 
  select="lib:if(true(), 'document(&quot;two/foo.xml&quot;)', 'document(&quot;two/bar.xml&quot;)')"
/>

<!-- ... -->

Теперь, если отслеживать обращения xslt-процессора к файловой системе, мы можем заметить, что в первом случае были вызваны оба файла из one/*.xml, тогда как во втором two/bar.xml вызван не был. Что нам и требовалось...

Использование XPath 2.0

В рамках XPath 2.0 для этого существует стандартный оператор if. С его использованием решение задачи будет таким: if($cond)then $one else $two.

Это решение лишено всех недостатков варинтов, приведенных выше, и может использоваться в любой из перечисленных ситуаций. Так что если есть возможность пользоваться XSLT 2.0 — следует использовать именно этот вариант.

Oбсуждения по теме в форуме

Тернарный оператор в XSLT?
XPath: document() при условии

Powered by POEM™ Engine Copyright © 2002-2005