Coersão pra Boolean

Posted Over 6 years ago. Visible to the public.

Existem basicamente duas perguntas que justifiquem a conversão de um objeto qualquer pra boolean:

  1. esse é um objeto "válido"?
  2. esse é um objeto que representa um boolean serializado?

O caso [2] só se aplica a inteiros (0 ou 1), strings ('true' ou 'false'), e booleans propriamente ditos (true, false). Um nível aceitável de coersão implícita é o null, que é por padrão considerado como false (e como consequência, 'null' é considerado uma representação do boolean false).

O caso [1] se aplica a qualquer objeto, e a interpretação é subjetiva e depende do domínio. É considerada boa prática aceitar que, para casos de string, a interpretação de uma string válida seja "uma string que seja não-nula e não-vazia".
Já o caso [2], pra strings, abrange representações como 'y', 'yes' e 'on', que podem vir da serialização de um input[checkbox], ou de um formulário qualquer, por exemplo.

Existem várias alternativas pra executar cada uma dessas alternativas em Groovy/Java, algumas mais verbosas que as outras, mas existem extrapolações na subjetividade do que pode representar um boolean serializado que precisam de atenção.

Considere o seguinte script groovy que testa vários objetos diferentes em várias alternativas de coersão de objeto para boolean:

final List<List<Object>> x = []
final List<Object> strings = [
		'', '0', 'n', 'no', 'off', 'f', 'false', 'FALSE', 'FaLsE',
		'what', '1', 'y', 'yes', 'on', 't', 'true', 'TRUE', 'tRuE',
		false, true, null, 0, 1, [], [''], [:], [a: ''],
]

Object handleError(Closure cl) {
	try {
		return cl()
	} catch (Exception ignored) {
		return 'ERRO'
	}
}

for (final Object s in strings) {
	final List<Object> _x = []

	_x.add(handleError { !!s })
	_x.add(handleError { s as Boolean })
	_x.add(handleError { s?.asBoolean() })
	_x.add(handleError { s?.toBoolean() })
	_x.add(handleError { new Boolean(s as String) })

	x.add(_x)
}
final List<String> options = ["!!s", "s as Boolean", "s?.asBoolean()", "s?.toBoolean()", "new Boolean(s as String)"]
final int biggestOptionLength = options.collect { it.length() }.max()
final List<String> t = options.collect { it.center(biggestOptionLength, ' ') }

println(("=" * biggestOptionLength) + strings.collect { it.inspect().center(8, ' ') }.join(""))

x.transpose().eachWithIndex { final List<Object> r, final int idx ->
	println(t[idx] + r.collect { it.inspect().center(8, ' ') }.join(""))
}

return

O resultado da execução acima é:


========================   ''     '0'     'n'     'no'   'off'    'f'   'false' 'FALSE' 'FaLsE'  'what'   '1'     'y'    'yes'    'on'    't'    'true'  'TRUE'  'tRuE'  false    true    null     0       1       []     ['']    [:]   ['a':'']
          !!s            false    true    true    true    true    true    true    true    true    true    true    true    true    true    true    true    true    true   false    true   false   false    true   false    true   false    true  
      s as Boolean       false    true    true    true    true    true    true    true    true    true    true    true    true    true    true    true    true    true   false    true    null   false    true   false    true   false    true  
     s?.asBoolean()      false    true    true    true    true    true    true    true    true    true    true    true    true    true    true    true    true    true   false    true    null   false    true   false    true   false    true  
     s?.toBoolean()      false   false   false   false   false   false   false   false   false   false    true    true   false   false   false    true    true    true   false    true    null   'ERRO'  'ERRO'  'ERRO'  'ERRO'  'ERRO'  'ERRO' 
new Boolean(s as String) false   false   false   false   false   false   false   false   false   false   false   false   false   false   false    true    true    true   false    true   false   false   false   false   false   false   false  

Observações relevantes:

  • s?.toBoolean() é a alternativa mais perigosa, abrangente, e instável. Ela não está disponível para todos os tipos de objetos, e precisa de um safe navigator pra ser parcialmente seguro (NPE). A coersão feita por esse método é dependende de domínio, e a sugestão é que, se a string em questão pode conter uma representação não convencional de um boolean, que a conversão/coersão seja feita manualmente, explicitamente, por quem conhece do domínio do objeto.
  • as três primeiras alternativas são equivalentes (chegam ao mesmo ponto na cadeia de execução do groovy), e representam fielmente o caso [1] citado no começo do card, sendo que a alternativa s as Boolean possui o menor peso sintático, pois não requer um safe navigator e expõe explicitamente pra IDE qual o tipo da coersão (e por isso, apesar de todas serem equivalentes, ela é a recomendada quando legibilidade e null-safety forem prioridades).
  • new Boolean(s as String) é a maneira mais limpa de representar o caso [2], em que busca-se saber se um objeto é a representação de um boolean serializado. Perceba que existe uma coersão interna explícita s as String que tem quase zero de impacto na performance, aumenta significativamente a confiança do código, dá ao compilador/IDE informação suficiente pra eliminar ambiguidade na chamada do construtor, e não sofre de problemas de coersão como NPE e/ou NoSuchMethod. Portanto, a coersão pra string é necessária pra qualidade de código ótima.

Observações/recomendações adicionais:

  • sempre que possível, trate/diferencie devidamente o valor null em relação aos otros possíveis valores do objeto, independente do tipo esperado do mesmo.
  • sempre que possível, descreva de maneira explícita a interpretação esperada do que é um objeto "válido", como por exemplo: s != null, ou s != null && s.length() != 0 ou até s != null && s.trim().length() != 0
  • contextos naturalmente booleanos, como ifs ou a condição de um for, sempre farão a coersão asType do groovy, que fará booleanUnbox, executando uma lógica relativamente extensa (apesar de otimizada e simples) até chegar em uma representação do "groovy truth" do objeto. Essa cadeia de execução tem impacto significativo no tempo de execução e consequentemente na performance do código, caso ele seja chamado com alta frequência. Por conta disso, o ponto anterior é importante: seja explícito sempre que possível, evite coersões implícitas em contextos booleanos.
  • conheça o domínio do objeto quando ele não for trivial. Anote/documente o domínio se ele não for trivial. Enforce/reforce um domínio restrito se possível, o mais cedo possível.
Frederico Galvão
Last edit
Over 6 years ago
Frederico Galvão
Posted by Frederico Galvão to ZeroGlosa (2017-08-31 21:51)