Pergunta
Embora a especificação C# inclua um pré-processador e diretivas básicas (#define, #if, etc), a linguagem não possui o mesmo pré-processador flexível encontrado em linguagens como C/C++.Acredito que a falta de um pré-processador tão flexível foi uma decisão de design tomada por Anders Hejlsberg (embora, infelizmente, não consiga encontrar referência a isso agora).Por experiência própria, esta é certamente uma boa decisão, já que havia algumas macros realmente terríveis e insustentáveis criadas quando eu estava fazendo muito C/C++.
Dito isso, há vários cenários em que eu poderia achar útil um pré-processador um pouco mais flexível.Código como o seguinte poderia ser melhorado por algumas diretivas simples de pré-processador:
public string MyProperty
{
get { return _myProperty; }
set
{
if (value != _myProperty)
{
_myProperty = value;
NotifyPropertyChanged("MyProperty");
// This line above could be improved by replacing the literal string with
// a pre-processor directive like "#Property", which could be translated
// to the string value "MyProperty" This new notify call would be as follows:
// NotifyPropertyChanged(#Property);
}
}
}
Seria uma boa ideia escrever um pré-processador para lidar com casos extremamente simples como este?Steve McConnell escreveu em Código completo (pág.208):
Escreva seu próprio pré-processador Se uma linguagem não inclui um pré-processador, é bastante fácil escrever um...
Eu estou rasgado.Foi uma decisão de design deixar um pré-processador tão flexível fora do C#.No entanto, um autor que respeito muito menciona que pode ser aceitável em algumas circunstâncias.
Devo construir um pré-processador C#?Existe algum disponível que faça as coisas simples que eu quero fazer?
Solução
Considere dar uma olhada em uma solução orientada a aspectos como PostSharp, que injeta código após o fato com base em atributos personalizados.É o oposto de um pré-compilador, mas pode fornecer o tipo de funcionalidade que você procura (notificações de PropertyChanged, etc.).
Outras dicas
Devo construir um pré-processador C#?Existe algum disponível que faça as coisas simples que eu quero fazer?
Você sempre pode usar o pré-processador C - C# é próximo o suficiente em termos de sintaxe.M4 também é uma opção.
Eu sei que muitas pessoas pensam que código curto é igual a código elegante, mas isso não é verdade.
O exemplo que você propõe está perfeitamente resolvido em código, como você mostrou, para que você precisa de uma diretiva de pré-processador?Você não deseja "pré-processar" seu código, deseja que o compilador insira algum código para você em suas propriedades.É um código comum, mas esse não é o propósito do pré-processador.
Com o seu exemplo, onde você coloca o limite?É claro que isso satisfaz um padrão de observador e não há dúvida de que será útil, mas há muitas coisas que seriam úteis e que são realmente feitas porque o código fornece flexibilidade onde o pré-processador não.Se você tentar implementar padrões comuns através de diretivas de pré-processador, você terminará com um pré-processador que precisa ser tão poderoso quanto a própria linguagem.Se você quiser processe seu código de uma maneira diferente usando uma diretiva de pré-processador mas se você quiser apenas um trecho de código, encontre outra maneira, porque o pré-processador não foi feito para fazer isso.
Usando um pré-processador estilo C++, o código do OP pode ser reduzido a esta linha:
OBSERVABLE_PROPERTY(string, MyProperty)
OBSERVABLE_PROPERTY ficaria mais ou menos assim:
#define OBSERVABLE_PROPERTY(propType, propName) \
private propType _##propName; \
public propType propName \
{ \
get { return _##propName; } \
set \
{ \
if (value != _##propName) \
{ \
_##propName = value; \
NotifyPropertyChanged(#propName); \
} \
} \
}
Se você tiver 100 propriedades para lidar, isso significa aproximadamente 1.200 linhas de código vs.~100.O que é mais fácil de ler e entender?O que é mais fácil de escrever?
Com C#, supondo que você recorte e cole para criar cada propriedade, são 8 colas por propriedade, 800 no total.Com a macro, não é necessário colar nada.Qual tem maior probabilidade de conter erros de codificação?O que é mais fácil de alterar se você precisar adicionar, por exemplo.uma bandeira IsDirty?
As macros não são tão úteis quando é provável que haja variações personalizadas em um número significativo de casos.
Como qualquer ferramenta, as macros podem ser abusadas e podem até ser perigosas nas mãos erradas.Para alguns programadores, esta é uma questão religiosa, e os méritos de uma abordagem em detrimento de outra são irrelevantes;se for você, você deve evitar macros.Para aqueles de nós que usam ferramentas extremamente precisas de maneira regular, habilidosa e segura, as macros podem oferecer não apenas um ganho imediato de produtividade durante a codificação, mas também no downstream durante a depuração e a manutenção.
O principal argumento contra a construção de um pré-processador para C# é a integração no Visual Studio:seriam necessários muitos esforços (se possível) para que o intellisense e a nova compilação de plano de fundo funcionassem perfeitamente.
As alternativas são usar um plugin de produtividade do Visual Studio como ReSharper ou CódigoRush.Este último tem - até onde sei - um sistema de templates incomparável e vem com um excelente reestruturação ferramenta.
Outra coisa que pode ser útil para resolver os tipos exatos de problemas aos quais você está se referindo é uma estrutura AOP como PostSharp.
Você pode então usar atributos customizados para adicionar funcionalidades comuns.
Para obter o nome do método atualmente executado, você pode observar o rastreamento de pilha:
public static string GetNameOfCurrentMethod()
{
// Skip 1 frame (this method call)
var trace = new System.Diagnostics.StackTrace( 1 );
var frame = trace.GetFrame( 0 );
return frame.GetMethod().Name;
}
Quando você está em um método de conjunto de propriedades, o nome é set_Property.
Usando a mesma técnica, você também pode consultar o arquivo de origem e as informações de linha/coluna.
No entanto, não fiz benchmarking disso, criar o objeto stacktrace uma vez para cada conjunto de propriedades pode ser uma operação muito demorada.
Acho que possivelmente você está perdendo uma parte importante do problema ao implementar o INotifyPropertyChanged.Seu consumidor precisa de uma forma de determinar o nome da propriedade.Por esta razão você deve ter seus nomes de propriedades definidos como constantes ou strings estáticas somente leitura, desta forma o consumidor não precisa `adivinhar' os nomes das propriedades.Se você usasse um pré-processador, como o consumidor saberia qual é o nome da string da propriedade?
public static string MyPropertyPropertyName
public string MyProperty {
get { return _myProperty; }
set {
if (!String.Equals(value, _myProperty)) {
_myProperty = value;
NotifyPropertyChanged(MyPropertyPropertyName);
}
}
}
// in the consumer.
private void MyPropertyChangedHandler(object sender,
PropertyChangedEventArgs args) {
switch (e.PropertyName) {
case MyClass.MyPropertyPropertyName:
// Handle property change.
break;
}
}
Se eu estivesse projetando a próxima versão do C#, pensaria que cada função teria uma variável local incluída automaticamente contendo o nome da classe e o nome da função.Na maioria dos casos, o otimizador do compilador o eliminaria.
Não tenho certeza se há muita demanda por esse tipo de coisa.
@Jorge escreveu:Se você quiser processar seu código de uma maneira diferente, use uma diretiva de pré-processador, mas se você quiser apenas um trecho de código, encontre outra maneira, porque o pré-processador não foi feito para fazer isso.
Interessante.Eu realmente não considero que um pré-processador funcione necessariamente dessa maneira.No exemplo fornecido, estou fazendo uma simples substituição de texto, que está alinhada com a definição de um pré-processador em Wikipédia.
Se este não for o uso adequado de um pré-processador, como deveríamos chamar uma simples substituição de texto, que geralmente precisa ocorrer antes de uma compilação?
Pelo menos para o cenário fornecido, há uma solução mais limpa e de tipo seguro do que construir um pré-processador:
Use genéricos.Igual a:
public static class ObjectExtensions
{
public static string PropertyName<TModel, TProperty>( this TModel @this, Expression<Func<TModel, TProperty>> expr )
{
Type source = typeof(TModel);
MemberExpression member = expr.Body as MemberExpression;
if (member == null)
throw new ArgumentException(String.Format(
"Expression '{0}' refers to a method, not a property",
expr.ToString( )));
PropertyInfo property = member.Member as PropertyInfo;
if (property == null)
throw new ArgumentException(String.Format(
"Expression '{0}' refers to a field, not a property",
expr.ToString( )));
if (source != property.ReflectedType ||
!source.IsSubclassOf(property.ReflectedType) ||
!property.ReflectedType.IsAssignableFrom(source))
throw new ArgumentException(String.Format(
"Expression '{0}' refers to a property that is not a member of type '{1}'.",
expr.ToString( ),
source));
return property.Name;
}
}
Isso pode ser facilmente estendido para retornar um PropertyInfo
em vez disso, permitindo que você obtenha muito mais coisas do que apenas o nome da propriedade.
Já que é um Extension method
, você pode usar esse método em praticamente todos os objetos.
Além disso, isso é seguro para o tipo.
Não posso enfatizar isso o suficiente.
(Eu sei que é uma questão antiga, mas achei que faltava uma solução prática.)
Se você está pronto para abandonar o C#, você pode querer dar uma olhada no Vaia linguagem que tem incrivelmente flexível macro suporte através AST (Árvore de Sintaxe Abstrata).É realmente ótimo se você puder abandonar a linguagem C#.
Para obter mais informações sobre Boo, consulte estas questões relacionadas: